|
20475
|
886
|
1
|
2026-05-11T15:35:18.280950+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778513718280_m1.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.140625,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.14479166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.28125,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.28541666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.421875,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20462
|
NULL
|
NULL
|
NULL
|
|
20477
|
886
|
2
|
2026-05-11T15:35:48.773534+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778513748773_m1.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.140625,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.14479166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.28125,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.28541666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.421875,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42604166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5625,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56666666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.7027778,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.70694447,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.84305555,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8472222,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.47430557,"top":0.033333335,"width":0.05138889,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20462
|
NULL
|
NULL
|
NULL
|
|
20479
|
886
|
3
|
2026-05-11T15:36:19.136622+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778513779136_m1.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.140625,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.14479166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.28125,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.28541666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.421875,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42604166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5625,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56666666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.7027778,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.70694447,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.84305555,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8472222,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.47430557,"top":0.033333335,"width":0.05138889,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20462
|
NULL
|
NULL
|
NULL
|
|
20481
|
886
|
4
|
2026-05-11T15:36:49.510990+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778513809510_m1.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.140625,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.14479166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.28125,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.28541666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.421875,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42604166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5625,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56666666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.7027778,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.70694447,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.84305555,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8472222,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.47430557,"top":0.033333335,"width":0.05138889,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20462
|
NULL
|
NULL
|
NULL
|
|
20483
|
886
|
5
|
2026-05-11T15:37:19.669110+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778513839669_m1.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.140625,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.14479166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.28125,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.28541666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.421875,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42604166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5625,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56666666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.7027778,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.70694447,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.84305555,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8472222,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.47430557,"top":0.033333335,"width":0.05138889,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20462
|
NULL
|
NULL
|
NULL
|
|
20485
|
886
|
6
|
2026-05-11T15:37:50.125218+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778513870125_m1.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.140625,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.14479166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.28125,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.28541666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.421875,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42604166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5625,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56666666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.7027778,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.70694447,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.84305555,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8472222,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.47430557,"top":0.033333335,"width":0.05138889,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20462
|
NULL
|
NULL
|
NULL
|
|
20487
|
886
|
7
|
2026-05-11T15:38:20.276694+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778513900276_m1.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.140625,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.14479166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.28125,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.28541666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.421875,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42604166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5625,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56666666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.7027778,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.70694447,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.84305555,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8472222,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.47430557,"top":0.033333335,"width":0.05138889,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20462
|
NULL
|
NULL
|
NULL
|
|
20489
|
886
|
8
|
2026-05-11T15:38:50.485561+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778513930485_m1.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.140625,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.14479166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.28125,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.28541666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.421875,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42604166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5625,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56666666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ffmpeg","depth":2,"bounds":{"left":0.7027778,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.70694447,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.84305555,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8472222,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.47430557,"top":0.033333335,"width":0.05138889,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20462
|
NULL
|
NULL
|
NULL
|
|
20472
|
887
|
0
|
2026-05-11T15:34:44.060782+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778513684060_m2.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.33759972,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33959442,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.40492022,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4069149,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.4722407,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4742354,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.53956115,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5415558,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.60671544,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6087101,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.67386967,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.67586434,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.4973404,"top":1.0,"width":0.024601065,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20463
|
NULL
|
NULL
|
NULL
|
|
20474
|
887
|
1
|
2026-05-11T15:35:14.371542+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778513714371_m2.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.33759972,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33959442,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.40492022,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4069149,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.4722407,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4742354,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.53956115,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5415558,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.60671544,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6087101,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.67386967,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.67586434,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.4973404,"top":1.0,"width":0.024601065,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20463
|
NULL
|
NULL
|
NULL
|
|
20476
|
887
|
2
|
2026-05-11T15:35:44.574922+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778513744574_m2.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.33759972,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33959442,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.40492022,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4069149,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.4722407,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4742354,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.53956115,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5415558,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.60671544,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6087101,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.67386967,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.67586434,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.4973404,"top":1.0,"width":0.024601065,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20463
|
NULL
|
NULL
|
NULL
|
|
20478
|
887
|
3
|
2026-05-11T15:36:14.766824+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778513774766_m2.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.33759972,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33959442,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.40492022,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4069149,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.4722407,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4742354,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.53956115,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5415558,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.60671544,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6087101,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.67386967,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.67586434,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.4973404,"top":1.0,"width":0.024601065,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20463
|
NULL
|
NULL
|
NULL
|
|
20480
|
887
|
4
|
2026-05-11T15:36:45.037168+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778513805037_m2.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.33759972,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33959442,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.40492022,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4069149,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.4722407,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4742354,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.53956115,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5415558,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.60671544,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6087101,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.67386967,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.67586434,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.4973404,"top":1.0,"width":0.024601065,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20463
|
NULL
|
NULL
|
NULL
|
|
20482
|
887
|
5
|
2026-05-11T15:37:15.227525+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778513835227_m2.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.33759972,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33959442,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.40492022,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4069149,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.4722407,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4742354,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.53956115,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5415558,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.60671544,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6087101,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.67386967,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.67586434,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.4973404,"top":1.0,"width":0.024601065,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20463
|
NULL
|
NULL
|
NULL
|
|
20484
|
887
|
6
|
2026-05-11T15:37:45.439933+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778513865439_m2.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.33759972,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33959442,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.40492022,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4069149,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.4722407,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4742354,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.53956115,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5415558,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.60671544,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6087101,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.67386967,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.67586434,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.4973404,"top":1.0,"width":0.024601065,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20463
|
NULL
|
NULL
|
NULL
|
|
20486
|
887
|
7
|
2026-05-11T15:38:15.669890+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778513895669_m2.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.33759972,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33959442,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.40492022,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4069149,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.4722407,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4742354,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.53956115,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5415558,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.60671544,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6087101,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.67386967,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.67586434,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.4973404,"top":1.0,"width":0.024601065,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20463
|
NULL
|
NULL
|
NULL
|
|
20488
|
887
|
8
|
2026-05-11T15:38:45.910982+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778513925910_m2.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.33759972,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33959442,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.40492022,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4069149,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.4722407,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4742354,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.53956115,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5415558,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.60671544,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6087101,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.67386967,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.67586434,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.4973404,"top":1.0,"width":0.024601065,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20463
|
NULL
|
NULL
|
NULL
|
|
20493
|
888
|
0
|
2026-05-11T15:39:50.949362+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778513990949_m1.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.140625,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.14479166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.28125,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.28541666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.421875,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42604166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5625,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56666666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.7027778,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.70694447,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.84305555,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8472222,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.47430557,"top":0.033333335,"width":0.05138889,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20462
|
NULL
|
NULL
|
NULL
|
|
20495
|
888
|
1
|
2026-05-11T15:40:21.100945+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514021100_m1.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.140625,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.14479166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.28125,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.28541666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.421875,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42604166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5625,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56666666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.7027778,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.70694447,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.84305555,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8472222,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.47430557,"top":0.033333335,"width":0.05138889,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20462
|
NULL
|
NULL
|
NULL
|
|
20497
|
888
|
2
|
2026-05-11T15:40:51.321076+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514051321_m1.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.140625,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.14479166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.28125,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.28541666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.421875,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42604166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5625,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56666666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.7027778,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.70694447,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.84305555,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8472222,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.47430557,"top":0.033333335,"width":0.05138889,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20462
|
NULL
|
NULL
|
NULL
|
|
20499
|
888
|
3
|
2026-05-11T15:41:21.487174+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514081487_m1.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.140625,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.14479166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.28125,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.28541666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.421875,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42604166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5625,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56666666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.7027778,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.70694447,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.84305555,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8472222,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.47430557,"top":0.033333335,"width":0.05138889,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20462
|
NULL
|
NULL
|
NULL
|
|
20501
|
888
|
4
|
2026-05-11T15:41:51.619894+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514111619_m1.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.140625,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.14479166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.28125,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.28541666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.421875,"top":0.05888889,"width":0.140625,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42604166,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5625,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.56666666,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.7027778,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.70694447,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.84305555,"top":0.05888889,"width":0.14027777,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.8472222,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.47430557,"top":0.033333335,"width":0.05138889,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20462
|
NULL
|
NULL
|
NULL
|
|
20502
|
888
|
5
|
2026-05-11T15:41:54.253330+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514114253_m1.jpg...
|
PhpStorm
|
faVsco.js – ClientTest.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp‹ 40 llDOCKER+ +€ 881DEV (docker)₴2APP (-zsh)ScrmService->syncOpportunity('374720564');ScrmService-›matchByName('Robot');883APP (-zsh)-zsh-zsh*5screenpipe"100% <8• Mon 11 May 18:41:53T&1O ₴6-zsh*7 |+end diffAPPFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1Learn moreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfixdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diffPHPCS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.3.30Running analysis on 7 cores with 10 files per process.Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!Loadedconfig default from".php-cs-fixer.dist.php".5666/5666 [100%Fixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image » docker debug docker_lamp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ |...
|
NULL
|
6195251054195107988
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp‹ 40 llDOCKER+ +€ 881DEV (docker)₴2APP (-zsh)ScrmService->syncOpportunity('374720564');ScrmService-›matchByName('Robot');883APP (-zsh)-zsh-zsh*5screenpipe"100% <8• Mon 11 May 18:41:53T&1O ₴6-zsh*7 |+end diffAPPFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1Learn moreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfixdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diffPHPCS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.3.30Running analysis on 7 cores with 10 files per process.Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!Loadedconfig default from".php-cs-fixer.dist.php".5666/5666 [100%Fixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image » docker debug docker_lamp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ |...
|
20462
|
NULL
|
NULL
|
NULL
|
|
20504
|
888
|
6
|
2026-05-11T15:41:56.446249+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514116446_m1.jpg...
|
PhpStorm
|
faVsco.js – ClientTest.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8043719072324535154
|
-8628527368849355612
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
iTerm2ShellEditViewSessio Project: faVsco.js, menu
iTerm2ShellEditViewSessionScriptsProfilesWindowHelpDOCKERAPP (-zsh)-zsh++₴81DEV (docker)₴2APP (-zsh)ScrmService->syncOpportunity('374720564');ScrmService-›matchByName('Robot');-zsh> 0 hhl*5screenpipe"100% <78• Mon 11 May 18:41:55T₴1O ₴6-zsh*7 |+end diffAPPFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1Learn moreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfixdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diffPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.3.30Running analysis on 7 cores with 10 files per process.Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!Loadedconfig default from".php-cs-fixer.dist.php".5666/5666 [100%Fixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image » docker debug docker_1amp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ I...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
20507
|
888
|
7
|
2026-05-11T15:42:01.343178+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514121343_m1.jpg...
|
PhpStorm
|
faVsco.js – ClientTest.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
[{"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-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"ClientTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'ClientTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'ClientTest'","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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"144","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"11","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\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","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":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
4682894321548166980
|
4446428687123393012
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
20504
|
NULL
|
NULL
|
NULL
|
|
20508
|
888
|
8
|
2026-05-11T15:42:31.743679+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514151743_m1.jpg...
|
PhpStorm
|
faVsco.js – ClientTest.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
[{"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-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"ClientTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'ClientTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'ClientTest'","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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"144","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"11","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\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","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":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
4682894321548166980
|
4446428687123393012
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
20510
|
888
|
9
|
2026-05-11T15:43:02.076090+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514182076_m1.jpg...
|
PhpStorm
|
faVsco.js – ClientTest.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
[{"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-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"ClientTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'ClientTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'ClientTest'","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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"144","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"11","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\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","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":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
4682894321548166980
|
4446428687123393012
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
20508
|
NULL
|
NULL
|
NULL
|
|
20512
|
888
|
10
|
2026-05-11T15:43:32.431602+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514212431_m1.jpg...
|
PhpStorm
|
faVsco.js – ClientTest.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
[{"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-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"ClientTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'ClientTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'ClientTest'","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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"144","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"11","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\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","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":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
4682894321548166980
|
4446428687123393012
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
20508
|
NULL
|
NULL
|
NULL
|
|
20514
|
888
|
11
|
2026-05-11T15:44:02.732826+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514242732_m1.jpg...
|
PhpStorm
|
faVsco.js – ClientTest.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
[{"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-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"ClientTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'ClientTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'ClientTest'","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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"144","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"11","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\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","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":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
4682894321548166980
|
4446428687123393012
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
20508
|
NULL
|
NULL
|
NULL
|
|
20492
|
889
|
0
|
2026-05-11T15:39:46.387603+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778513986387_m2.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.33759972,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33959442,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.40492022,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4069149,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.4722407,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4742354,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.53956115,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5415558,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.60671544,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6087101,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.67386967,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.67586434,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.4973404,"top":1.0,"width":0.024601065,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20463
|
NULL
|
NULL
|
NULL
|
|
20494
|
889
|
1
|
2026-05-11T15:40:16.610898+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514016610_m2.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.33759972,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33959442,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.40492022,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4069149,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.4722407,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4742354,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.53956115,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5415558,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.60671544,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6087101,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.67386967,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.67586434,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.4973404,"top":1.0,"width":0.024601065,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20463
|
NULL
|
NULL
|
NULL
|
|
20496
|
889
|
2
|
2026-05-11T15:40:46.830135+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514046830_m2.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.33759972,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33959442,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.40492022,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4069149,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.4722407,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4742354,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.53956115,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5415558,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.60671544,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6087101,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.67386967,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.67586434,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.4973404,"top":1.0,"width":0.024601065,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20463
|
NULL
|
NULL
|
NULL
|
|
20498
|
889
|
3
|
2026-05-11T15:41:17.087854+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514077087_m2.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.33759972,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33959442,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.40492022,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4069149,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.4722407,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4742354,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.53956115,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5415558,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.60671544,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6087101,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.67386967,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.67586434,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.4973404,"top":1.0,"width":0.024601065,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20463
|
NULL
|
NULL
|
NULL
|
|
20500
|
889
|
4
|
2026-05-11T15:41:47.476011+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514107476_m2.jpg...
|
iTerm2
|
APP (-zsh)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/PlaybackService.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: resources/views/partials/crm/push-summary/html-assembly.blade.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1482, done.\nremote: Counting objects: 100% (481/481), done.\nremote: Compressing objects: 100% (191/191), done.\nremote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)\nReceiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.\nResolving deltas: 100% (877/877), completed with 96 local objects.\nFrom github.com:jiminny/app\n 83b628967a..ad2ce76737 master -> origin/master\n 1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n 5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests\n b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n * [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost\n * [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc\n * [new branch] make-claude-great-again -> origin/make-claude-great-again\n * [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507\n * [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tresources/views/partials/crm/push-summary/html-assembly.blade.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Jobs/Team/SyncToIntercom.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 83b628967a..ad2ce76737\nFast-forward\n .cursor/rules/frontend-conventions.mdc | 23 ++\n .env.production-eu | 2 +-\n .env.staging | 2 +-\n Makefile | 10 +\n app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-\n app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-\n app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--\n app/Component/AskAnything/AskAnythingPromptService.php | 3 +\n app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-\n app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-\n app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---\n app/Component/Twilio/TwilioRepository.php | 27 ++\n app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----\n app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--\n app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++\n app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----\n app/Console/Commands/Users/SyncToIntercom.php | 4 +-\n app/Console/Kernel.php | 3 +-\n app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -\n app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -\n app/Contracts/Repositories/TeamRepository.php | 3 +-\n app/Events/Activities/ActivityUpdated.php | 10 +-\n app/Events/Activities/Audio/RecordingEvent.php | 6 +-\n app/Events/Activities/Softphone/Ended.php | 8 +-\n app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-\n app/Events/Activities/Softphone/Started.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 17 +-\n app/Http/Controllers/API/SoftphoneController.php | 9 +-\n app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-\n app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-\n app/Http/Controllers/Auth/SocialController.php | 6 +-\n app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-\n app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-\n app/Http/Controllers/Kiosk/PartnersController.php | 46 +++\n app/Http/Controllers/Kiosk/SearchController.php | 8 +\n app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-\n app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-\n app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/ActivityTransformer.php | 4 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PartnerTransformer.php | 1 +\n app/Http/Transformers/StageTransformer.php | 6 +-\n app/Http/Transformers/UserTransformer.php | 11 +-\n app/Interactions/Settings/Teams/CreateTeam.php | 3 +\n app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-\n app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++\n app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-\n app/Jobs/Crm/UpdateStage.php | 3 +\n app/Jobs/Team/SyncToIntercom.php | 7 +-\n app/Listeners/Teams/SyncIntercomCompany.php | 5 +-\n app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-\n app/Listeners/Users/SyncIntercom.php | 5 +-\n app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++\n app/Mail/Reports/ReportNotGenerated.php | 41 +++\n app/Models/Activity.php | 25 +-\n app/Models/Activity/Question.php | 14 +-\n app/Models/Activity/Search.php | 7 +\n app/Models/AskAnything/AskAnythingPrompt.php | 6 +\n app/Models/AutomatedReport.php | 10 +\n app/Models/CoachingFeedback.php | 44 ++-\n app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----\n app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----\n app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --\n app/Models/Partner.php | 13 +\n app/Models/Playlist/Activity.php | 14 +-\n app/Notifications/OwnerInvitedToTrial.php | 14 +-\n app/Policies/UserPolicy.php | 16 +-\n app/Queue/Worker/Worker.php | 3 +-\n app/Repositories/ActivityRepository.php | 13 +-\n app/Repositories/AutomatedReportsRepository.php | 42 ++-\n app/Repositories/TeamRepository.php | 21 +-\n app/Repositories/UserRepository.php | 2 +-\n app/Services/Activity/MeetingBotService.php | 8 +-\n app/Services/ActivityService.php | 111 ++-----\n app/Services/Crm/Hubspot/Service.php | 36 +-\n app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--\n app/Services/Kiosk/KioskService.php | 7 +-\n app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-\n app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++\n app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++\n app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++\n app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----\n composer.json | 1 -\n composer.lock | 95 +-----\n config/secure-headers.php | 5 +-\n database/mappings/mapping_activities.json | 16 +\n database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++\n database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++\n database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++\n database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++\n front-end/package.json | 5 +-\n front-end/src/__mocks__/jiminny.js | 4 +-\n front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +\n front-end/src/__mocks__/setup.js | 1 +\n front-end/src/apps/ai-reports-promo.js | 22 ++\n front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++\n front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++\n front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++\n .../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++\n front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-\n front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++\n front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++\n .../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++\n front-end/src/components/AiReports/constants.js | 7 +\n front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +\n front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++\n front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +\n front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++\n front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-\n front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-\n front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-\n front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-\n front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++\n front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-\n front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++\n front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++\n front-end/src/main.js | 1 +\n front-end/src/store/modules/TeamInsights/util.js | 1 +\n front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++\n front-end/src/store/modules/platform/getters.js | 3 +\n front-end/src/utils/index.js | 11 +\n front-end/yarn.lock | 21 +-\n phpstan-baseline.neon | 60 ----\n public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes\n public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes\n public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes\n public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes\n public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes\n public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes\n public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes\n public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes\n resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++\n resources/views/emails/reports/report-not-generated.blade.php | 24 ++\n resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-\n routes/api.php | 6 +\n routes/web.php | 4 +\n tests/Feature/Policies/UserPolicyTest.php | 90 ++++-\n tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++\n tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++\n tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++\n tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-\n tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++\n tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++\n tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++\n tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++\n tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++\n tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++\n tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-\n tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++\n tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++\n tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +\n tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++\n tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-\n tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++\n tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++\n tests/Unit/Models/PartnerTest.php | 28 ++\n tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++\n tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-\n tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--\n tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++\n tests/Unit/Services/KioskServiceTest.php | 8 +\n tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-\n tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++\n tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++\n tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----\n 186 files changed, 8538 insertions(+), 1233 deletions(-)\n create mode 100644 app/Component/Twilio/TwilioRepository.php\n delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php\n create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php\n delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php\n create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php\n create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php\n create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php\n create mode 100644 app/Mail/Reports/ReportNotGenerated.php\n delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php\n create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php\n create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php\n create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php\n create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php\n create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php\n create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php\n create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php\n create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js\n create mode 100644 front-end/src/apps/ai-reports-promo.js\n create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js\n create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html\n create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js\n create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js\n create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js\n create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js\n create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/com/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf\n create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf\n create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf\n create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf\n create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf\n create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php\n create mode 100644 resources/views/emails/reports/report-not-generated.blade.php\n create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php\n create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php\n create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php\n create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php\n create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php\n create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php\n create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php\n create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php\n create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php\n create mode 100644 tests/Unit/Models/PartnerTest.php\n create mode 100644 tests/Unit/Services/ActivityServiceTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php\n create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 628, done.\nremote: Counting objects: 100% (331/331), done.\nremote: Compressing objects: 100% (63/63), done.\nremote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)\nReceiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.\nResolving deltas: 100% (391/391), completed with 57 local objects.\nFrom github.com:jiminny/app\n ad2ce76737..12295204cf master -> origin/master\n 14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3\n b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests\n f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import\n * [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n 1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech\n + ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)\n * [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication\n * [new branch] desktop-app -> origin/desktop-app\n 4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again\n * [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating ad2ce76737..12295204cf\nFast-forward\n app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-\n app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------\n app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------\n tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------\n tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -\n tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -\n 11 files changed, 49 insertions(+), 90 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric\nSwitched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ git status\nOn branch master\nYour branch is up to date with 'origin/master'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (2/2), done.\nremote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)\nUnpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.\nFrom github.com:jiminny/app\n c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit\nAlready up to date.\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull \nremote: Enumerating objects: 180, done.\nremote: Counting objects: 100% (179/179), done.\nremote: Compressing objects: 100% (55/55), done.\nremote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)\nReceiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.\nResolving deltas: 100% (127/127), completed with 47 local objects.\nFrom github.com:jiminny/app\n 12295204cf..35f036ace6 master -> origin/master\n * [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering\n f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue\n * [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories\n * [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration\n + ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)\n 8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas\nUpdating 12295204cf..35f036ace6\nFast-forward\n app/Component/Datadog/Constants.php | 1 +\n app/Providers/RouteServiceProvider.php | 2 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 4 files changed, 62 insertions(+), 8 deletions(-)\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr \n* master\n JY-20818-move-AJ-reports-to-separated-datadog-metric\n JY-20773-fix-automated-reports-user-pilot-tracking\n JY-20157-AJ-report-not-send-notification\n JY-20508-notify-before-AJ-report-expiration\n JY-20372-ai-reports-promotion-pages\n JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null\n JY-20738-debug-AJ-tracking-UP\n a\n JY-18909-automated-reports-ask-jiminny\n JY-20692-fix-integration-app-token-auth-response-change\n JY-20553-debug-crm-sync-delays\n JY-20698-fix-SF-activity-types-on-new-playbook\n JY-20543-AJ-report-tracking\n JY-20384-handle-auto-sync-with-no-access-to-event-type\n JY-20458-ask-jiminny-user-definitions\n JY-19666-fix-import-contacts-account-association\n JY-19666-HS-import-contacts-and-accounts-batch-job\n JY-20458-Ask-Jiminny-Reports\n JY-20200-batch-update-CRM-objects-Salesforce\n JY-19666-HS-webhooks-add-contact-and-company\n JY-20348-trigger-setup-DI-layout-on-team-creation\n JY-20326-refactor-info-message-in-command\n JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled\n JY-20312-remove-on-update-change-last-synced-at-crm-configurations\n JY-20306-SF-skip-auto-sync-for-task-based-playbook\n JY-20192-remove-deleted-team-from-saved-search-filters\n JY-20197-import-opportunity-batch-job\n JY-20293-enable-status-field-for-pipedrive-deals\n JY-20191-remove-commands-interactive-prompts\n JY-20118-change-default-sync-strategy\n JY-20183-add-cache-on-auto-log-delay\n JY-20197-add-import-opportunity-batch-job\n 20118-hs-opportunity-make-webhook-strategy-default\n JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based\n JY-20196-handle-opportunity-without-note\n JY-20118-improve-opportunity-import\n JY-20189-handle-activity-search-on-deleted-groups\n JY-20160\n JY-20145-filter-out-converted-leads-when-matching\n JY-20150-skip-push-summary-on-summary-ready-if-autolog\n JY-20132-fix-note-encoding\n JY-19792-clean-logs\n JY-20117-fix-sync-profile-fields-on-empty-fields\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit \nSwitched to a new branch 'JY-20725-handle-HS-search-rate-limit'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-emails:worker-emails_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) app/Jobs/Crm/MatchActivityCrmData.php (ordered_class_elements)\n ---------- begin diff ----------\n--- /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n+++ /home/jiminny/app/Jobs/Crm/MatchActivityCrmData.php\n@@ -29,24 +29,14 @@\n use InteractsWithQueue;\n use SerializesModels;\n \n+ private const int RETRY_WINDOW_MINUTES = 120;\n+\n public int $maxExceptions = 3;\n \n- private const int RETRY_WINDOW_MINUTES = 120;\n-\n private int $activityId;\n private ?Configuration $fromConfiguration;\n private bool $remoteSearch;\n \n- public function middleware(): array\n- {\n- return [new HandleHubspotRateLimit()];\n- }\n-\n- public function retryUntil(): \\DateTimeInterface\n- {\n- return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n- }\n-\n public function __construct(\n int $activityId,\n ?Configuration $fromConfiguration = null,\n@@ -57,6 +47,16 @@\n $this->remoteSearch = $remoteSearch;\n \n $this->onQueue(Constants::QUEUE_ANALYTICS_LOW);\n+ }\n+\n+ public function middleware(): array\n+ {\n+ return [new HandleHubspotRateLimit()];\n+ }\n+\n+ public function retryUntil(): \\DateTimeInterface\n+ {\n+ return now()->addMinutes(self::RETRY_WINDOW_MINUTES);\n }\n \n public function uniqueId(): string\n\n ----------- end diff -----------\n\n 2) app/Services/Crm/Hubspot/HubspotClientInterface.php (phpdoc_order, phpdoc_separation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/HubspotClientInterface.php\n@@ -46,9 +46,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array;\n }\n\n ----------- end diff -----------\n\n 3) app/Services/Crm/Hubspot/Client.php (phpdoc_order, phpdoc_separation, blank_line_before_statement)\n ---------- begin diff ----------\n--- /home/jiminny/app/Services/Crm/Hubspot/Client.php\n+++ /home/jiminny/app/Services/Crm/Hubspot/Client.php\n@@ -69,10 +69,12 @@\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n+ *\n * @param callable(): T $apiCall The API call to execute\n- * @return T The result of the API call\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n+ *\n+ * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n@@ -81,6 +83,7 @@\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n+\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n@@ -232,9 +235,11 @@\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n- * @return array The search response with 'results', 'total', 'paging' keys\n+ *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n+ *\n+ * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n\n ----------- end diff -----------\n\n 4) app/Console/Commands/JiminnyDebugCommand.php (statement_indentation)\n ---------- begin diff ----------\n--- /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n+++ /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php\n@@ -359,11 +359,11 @@\n $crmService = $crmResolver->prepareCrmService();\n \n for ($i = 0 ; $i < 3; $i++) {\n-// if ($i % 25 === 0) {\n-// $this->info(\"Syncing opportunity {$i}\");\n- $this->info(\"Matching contact {$i}\");\n-// }\n-// $crmService->syncOpportunity('374720564');\n+ // if ($i % 25 === 0) {\n+ // $this->info(\"Syncing opportunity {$i}\");\n+ $this->info(\"Matching contact {$i}\");\n+ // }\n+ // $crmService->syncOpportunity('374720564');\n $crmService->matchByName('Robot');\n }\n }\n\n ----------- end diff -----------\n\n\nFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix \ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 106.192 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5666/5666 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.33759972,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33959442,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.40492022,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4069149,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.4722407,"top":1.0,"width":0.06732048,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.4742354,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.53956115,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5415558,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.60671544,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.6087101,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.67386967,"top":1.0,"width":0.06715426,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.67586434,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.4973404,"top":1.0,"width":0.024601065,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-5826347064466025141
|
-2292063823433160584
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys007
Poetry Last login: Thu May 7 09:44:56 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: app/Jobs/Team/SyncToIntercom.php
modified: app/Services/PlaybackService.php
modified: config/logging.php
modified: resources/views/partials/crm/push-summary/html-assembly.blade.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1482, done.
remote: Counting objects: 100% (481/481), done.
remote: Compressing objects: 100% (191/191), done.
remote: Total 1482 (delta 349), reused 305 (delta 289), pack-reused 1001 (from 4)
Receiving objects: 100% (1482/1482), 1017.97 KiB | 1.44 MiB/s, done.
Resolving deltas: 100% (877/877), completed with 96 local objects.
From github.com:jiminny/app
83b628967a..ad2ce76737 master -> origin/master
1ee8cbcb7b..14f54b5be2 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
5662c3b32f..b167b19973 JY-20289-api-tests -> origin/JY-20289-api-tests
b40408cfad..f23cfee7c3 JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
* [new branch] JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20662-remove-word-boost -> origin/JY-20662-remove-word-boost
* [new branch] JY-20742-mcp-poc -> origin/JY-20742-mcp-poc
* [new branch] make-claude-great-again -> origin/make-claude-great-again
* [new branch] secfix/composer-20260507 -> origin/secfix/composer-20260507
* [new branch] secfix/npm-20260507 -> origin/secfix/npm-20260507
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
resources/views/partials/crm/push-summary/html-assembly.blade.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
error: Your local changes to the following files would be overwritten by merge:
app/Jobs/Team/SyncToIntercom.php
Please commit your changes or stash them before you merge.
Aborting
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
Updating 83b628967a..ad2ce76737
Fast-forward
.cursor/rules/frontend-conventions.mdc | 23 ++
.env.production-eu | 2 +-
.env.staging | 2 +-
Makefile | 10 +
app/Component/ActivityAnalytics/Service/ActivityAnalyticsService.php | 6 +-
app/Component/AiAutomation/Repositories/AiTemplateFieldsRepository.php | 32 +-
app/Component/AiCallScoring/Repositories/AiScorecardRepository.php | 56 ++--
app/Component/AskAnything/AskAnythingPromptService.php | 3 +
app/Component/Transcription/Job/FinishTranscriptionJob.php | 37 ++-
app/Component/Transcription/TranscriptionProcessor/Gong/Gong.php | 18 +-
app/Component/Twilio/Conference/ConferenceManager/SoftPhoneManager.php | 4 +-
app/Component/Twilio/Service/SoftPhoneService.php | 124 ++++---
app/Component/Twilio/TwilioRepository.php | 27 ++
app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php | 59 ----
app/Console/Commands/Reports/AutomatedReportsCommand.php | 122 +++++--
app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php | 200 ++++++++++++
app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php | 60 ----
app/Console/Commands/Users/SyncToIntercom.php | 4 +-
app/Console/Kernel.php | 3 +-
app/Contracts/ES/Events/UpdateMultipleEntities.php | 4 -
app/Contracts/ES/Events/UpdateSingleEntity.php | 4 -
app/Contracts/Repositories/TeamRepository.php | 3 +-
app/Events/Activities/ActivityUpdated.php | 10 +-
app/Events/Activities/Audio/RecordingEvent.php | 6 +-
app/Events/Activities/Softphone/Ended.php | 8 +-
app/Events/Activities/Softphone/SoftphoneEvent.php | 24 +-
app/Events/Activities/Softphone/Started.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 17 +-
app/Http/Controllers/API/SoftphoneController.php | 9 +-
app/Http/Controllers/API/UserAutomatedReports/UserAutomatedReportsController.php | 19 +-
app/Http/Controllers/API/V2/AskAnythingController.php | 2 +-
app/Http/Controllers/Auth/SocialController.php | 6 +-
app/Http/Controllers/Kiosk/AutomatedReportsController.php | 38 ++-
app/Http/Controllers/Kiosk/OrganizationsController.php | 8 +-
app/Http/Controllers/Kiosk/PartnersController.php | 46 +++
app/Http/Controllers/Kiosk/SearchController.php | 8 +
app/Http/Controllers/Kiosk/Teams/OnboardController.php | 24 +-
app/Http/Controllers/Settings/Teams/IntegrationController.php | 6 +-
app/Http/Controllers/TeamSetupController.php | 4 +-
app/Http/Controllers/Telephony/TextMessaging/MessageController.php | 12 +-
app/Http/Controllers/Telephony/TextMessaging/WebhookController.php | 18 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 1 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/ActivityTransformer.php | 4 +-
app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-
app/Http/Transformers/PartnerTransformer.php | 1 +
app/Http/Transformers/StageTransformer.php | 6 +-
app/Http/Transformers/UserTransformer.php | 11 +-
app/Interactions/Settings/Teams/CreateTeam.php | 3 +
app/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJob.php | 80 ++++-
app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php | 119 +++++++
app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php | 89 +++++
app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 12 +-
app/Jobs/Crm/UpdateStage.php | 3 +
app/Jobs/Team/SyncToIntercom.php | 7 +-
app/Listeners/Teams/SyncIntercomCompany.php | 5 +-
app/Listeners/Teams/UpdateSalesforceAccount.php | 8 +-
app/Listeners/Users/SyncIntercom.php | 5 +-
app/Mail/Reports/AskJiminnyReportExpiringMail.php | 40 +++
app/Mail/Reports/ReportNotGenerated.php | 41 +++
app/Models/Activity.php | 25 +-
app/Models/Activity/Question.php | 14 +-
app/Models/Activity/Search.php | 7 +
app/Models/AskAnything/AskAnythingPrompt.php | 6 +
app/Models/AutomatedReport.php | 10 +
app/Models/CoachingFeedback.php | 44 ++-
app/Models/ElasticSearch/ActivityElasticSearchTrait.php | 86 +----
app/Models/ElasticSearch/OpportunityElasticSearchTrait.php | 71 ----
app/Models/ElasticSearch/SharedDocumentDeleteTrait.php | 27 --
app/Models/Partner.php | 13 +
app/Models/Playlist/Activity.php | 14 +-
app/Notifications/OwnerInvitedToTrial.php | 14 +-
app/Policies/UserPolicy.php | 16 +-
app/Queue/Worker/Worker.php | 3 +-
app/Repositories/ActivityRepository.php | 13 +-
app/Repositories/AutomatedReportsRepository.php | 42 ++-
app/Repositories/TeamRepository.php | 21 +-
app/Repositories/UserRepository.php | 2 +-
app/Services/Activity/MeetingBotService.php | 8 +-
app/Services/ActivityService.php | 111 ++-----
app/Services/Crm/Hubspot/Service.php | 36 +-
app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php | 2 +-
app/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityService.php | 5 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 49 +--
app/Services/Kiosk/KioskService.php | 7 +-
app/Services/Webhook/Triggers/AiScorecardCompletedTrigger.php | 13 +-
app/UseCases/TeamInsights/ConversationRowMapper.php | 78 +++++
app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php | 68 ++++
app/UseCases/TeamInsights/StrictConsentColumnResolver.php | 45 +++
app/UseCases/TeamInsights/TeamConversationsExport.php | 154 ++++-----
composer.json | 1 -
composer.lock | 95 +-----
config/secure-headers.php | 5 +-
database/mappings/mapping_activities.json | 16 +
database/migrations/2026_04_14_000000_add_rockeed_partner.php | 51 +++
database/migrations/2026_04_22_000000_add_success_email_to_partners.php | 26 ++
database/migrations/2026_04_27_000000_add_label_to_partners.php | 28 ++
database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php | 79 +++++
front-end/package.json | 5 +-
front-end/src/__mocks__/jiminny.js | 4 +-
front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js | 9 +
front-end/src/__mocks__/setup.js | 1 +
front-end/src/apps/ai-reports-promo.js | 22 ++
front-end/src/components/AiReports/AiReportsPromo.vue | 22 ++
front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue | 190 +++++++++++
front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue | 111 +++++++
front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue | 103 ++++++
front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js | 98 ++++++
.../src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html | 283 ++++++++++++++++
front-end/src/components/AiReports/Manage/ManageAiReports.vue | 8 +-
front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue | 228 +++++++++++++
front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js | 71 ++++
.../src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html | 217 ++++++++++++
front-end/src/components/AiReports/constants.js | 7 +
front-end/src/components/Settings/Kiosk/OrganizationSearch/Organizations.vue | 1 +
front-end/src/components/Settings/Kiosk/__mocks__/Jiminny.js | 1 +
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 43 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 203 ++++++++++++
front-end/src/components/Settings/Kiosk/shared/Navigation/Navigation.vue | 3 +
front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js | 67 ++++
front-end/src/components/TeamInsights/CoachingFrameworks/AICallScoring/aiCallScoringOverTime.ts | 4 +-
front-end/src/components/TeamInsights/CoachingFrameworks/UsersList.vue | 2 +-
front-end/src/components/layout/Sidebar/HelpMenu.vue | 25 +-
front-end/src/components/layout/Sidebar/Sidebar.vue | 27 +-
front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js | 94 ++++++
front-end/src/components/layout/Sidebar/__tests__/__snapshots__/Sidebar.spec.js.snap | 4 +-
front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js | 204 ++++++++++++
front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js | 49 +++
front-end/src/main.js | 1 +
front-end/src/store/modules/TeamInsights/util.js | 1 +
front-end/src/store/modules/platform/__tests__/getters.spec.js | 22 ++
front-end/src/store/modules/platform/getters.js | 3 +
front-end/src/utils/index.js | 11 +
front-end/yarn.lock | 21 +-
phpstan-baseline.neon | 60 ----
public/pdf/exec-reports/com/coaching-profiles.pdf | Bin 0 -> 1531178 bytes
public/pdf/exec-reports/com/exec-summary.pdf | Bin 0 -> 2237381 bytes
public/pdf/exec-reports/com/loss-report.pdf | Bin 0 -> 1955343 bytes
public/pdf/exec-reports/com/product-feedback.pdf | Bin 0 -> 2184417 bytes
public/pdf/exec-reports/eu/coaching-profiles.pdf | Bin 0 -> 1528704 bytes
public/pdf/exec-reports/eu/exec-summary.pdf | Bin 0 -> 2296741 bytes
public/pdf/exec-reports/eu/loss-report.pdf | Bin 0 -> 1955808 bytes
public/pdf/exec-reports/eu/product-feedback.pdf | Bin 0 -> 2184083 bytes
resources/views/emails/reports/ask-jiminny-report-expiring.blade.php | 22 ++
resources/views/emails/reports/report-not-generated.blade.php | 24 ++
resources/views/partials/crm/push-summary/html-assembly.blade.php | 2 +-
routes/api.php | 6 +
routes/web.php | 4 +
tests/Feature/Policies/UserPolicyTest.php | 90 ++++-
tests/Unit/Component/ActivityAnalytics/Service/ActivityAnalyticsServiceTest.php | 40 +++
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 26 ++
tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php | 276 ++++++++++++++++
tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php | 375 +++++++++++++++++++++
tests/Unit/Component/Twilio/Service/SoftPhoneServiceTest.php | 1014 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
tests/Unit/Console/Commands/Reports/AutomatedReportsCommandTest.php | 157 ++++++++-
tests/Unit/Events/Activities/Audio/RecordingEventTest.php | 72 ++++
tests/Unit/Events/Activities/Softphone/EndedTest.php | 86 +++++
tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php | 88 +++++
tests/Unit/Events/Activities/Softphone/StartedTest.php | 86 +++++
tests/Unit/Http/Controllers/Kiosk/AutomatedReportsControllerTest.php | 99 ++++++
tests/Unit/Http/Transformers/ActivityTransformerTest.php | 5 +-
tests/Unit/Http/Transformers/PartnerTransformerTest.php | 34 ++
tests/Unit/Interactions/Settings/Teams/CreateTeamTest.php | 49 +++
tests/Unit/Jobs/AutomatedReports/RequestGenerateAskJiminnyReportJobTest.php | 106 +++++-
tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php | 205 ++++++++++++
tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php | 188 +++++++++++
tests/Unit/Jobs/Crm/ImportOpportunityBatchTest.php | 2 +-
tests/Unit/Jobs/Team/SyncToIntercomTest.php | 6 +
tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php | 59 ++++
tests/Unit/Listeners/Teams/UpdateSalesforceAccountTest.php | 11 +-
tests/Unit/Listeners/Users/SyncIntercomTest.php | 59 ++++
tests/Unit/Mail/Reports/ReportNotGeneratedTest.php | 166 ++++++++++
tests/Unit/Models/PartnerTest.php | 28 ++
tests/Unit/Repositories/AutomatedReportsRepositoryTest.php | 68 ++++
tests/Unit/Services/Activity/MeetingBotServiceRequestRecordingToStopTest.php | 14 +-
tests/Unit/Services/ActivityServiceTest.php | 391 ++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ServiceResponseNormalizeTest.php | 68 ++--
tests/Unit/Services/Kiosk/AutomatedReports/AskJiminnyReportActivityServiceTest.php | 48 +--
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceActivitiesCountTest.php | 16 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceReportGenerationTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 130 ++++++++
tests/Unit/Services/KioskServiceTest.php | 8 +
tests/Unit/Services/Webhook/Triggers/AiScorecardCompletedTriggerTest.php | 6 +-
tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php | 119 +++++++
tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php | 108 ++++++
tests/Unit/UseCases/TeamInsights/TeamConversationsExportTest.php | 342 ++++++++++++++-----
186 files changed, 8538 insertions(+), 1233 deletions(-)
create mode 100644 app/Component/Twilio/TwilioRepository.php
delete mode 100644 app/Console/Commands/CoachingFeedbacksUpdateEsActivities.php
create mode 100644 app/Console/Commands/RunAiCallScoringForUntypedActivitiesCommand.php
delete mode 100644 app/Console/Commands/UpdateActivitiesAverageScoreExcludingFeedbacksNotSetVisibleToAll.php
create mode 100644 app/Http/Controllers/Kiosk/PartnersController.php
create mode 100644 app/Jobs/AutomatedReports/SendReportExpiringSoonMailJob.php
create mode 100644 app/Jobs/AutomatedReports/SendReportNotGeneratedMailJob.php
create mode 100644 app/Mail/Reports/AskJiminnyReportExpiringMail.php
create mode 100644 app/Mail/Reports/ReportNotGenerated.php
delete mode 100644 app/Models/ElasticSearch/SharedDocumentDeleteTrait.php
create mode 100644 app/UseCases/TeamInsights/ConversationRowMapper.php
create mode 100644 app/UseCases/TeamInsights/RecordingOutcomeTextResolver.php
create mode 100644 app/UseCases/TeamInsights/StrictConsentColumnResolver.php
create mode 100644 database/migrations/2026_04_14_000000_add_rockeed_partner.php
create mode 100644 database/migrations/2026_04_22_000000_add_success_email_to_partners.php
create mode 100644 database/migrations/2026_04_27_000000_add_label_to_partners.php
create mode 100644 database/migrations/2026_04_29_105053_move_ask_jiminny_reports_to_grow_tier.php
create mode 100644 front-end/src/__mocks__/kit/endpoints/automated-reports-promo.js
create mode 100644 front-end/src/apps/ai-reports-promo.js
create mode 100644 front-end/src/components/AiReports/AiReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/AutomatedReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/PromoCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/WhyItMattersCard.vue
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/AutomatedReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/AutomatedReportsPromo/__tests__/__snapshots__/automated-reports-promo.output.html
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/PanoramaReportsPromo.vue
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/PanoramaReportsPromo.spec.js
create mode 100644 front-end/src/components/AiReports/PanoramaReportsPromo/__tests__/__snapshots__/panorama-reports-promo.output.html
create mode 100644 front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js
create mode 100644 front-end/src/components/Settings/Kiosk/shared/Navigation/__tests__/Navigation.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/HelpMenu.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/__tests__/useAiReportsSidebarButton.spec.js
create mode 100644 front-end/src/components/layout/Sidebar/useAiReportsSidebarButton.js
create mode 100644 front-end/src/store/modules/platform/__tests__/getters.spec.js
create mode 100644 public/pdf/exec-reports/com/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/com/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/com/loss-report.pdf
create mode 100644 public/pdf/exec-reports/com/product-feedback.pdf
create mode 100644 public/pdf/exec-reports/eu/coaching-profiles.pdf
create mode 100644 public/pdf/exec-reports/eu/exec-summary.pdf
create mode 100644 public/pdf/exec-reports/eu/loss-report.pdf
create mode 100644 public/pdf/exec-reports/eu/product-feedback.pdf
create mode 100644 resources/views/emails/reports/ask-jiminny-report-expiring.blade.php
create mode 100644 resources/views/emails/reports/report-not-generated.blade.php
create mode 100644 tests/Unit/Component/Transcription/Job/FinishTranscriptionJobTest.php
create mode 100644 tests/Unit/Component/Transcription/TranscriptionProcessor/Gong/GongTest.php
create mode 100644 tests/Unit/Events/Activities/Audio/RecordingEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/EndedTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/SoftphoneEventTest.php
create mode 100644 tests/Unit/Events/Activities/Softphone/StartedTest.php
create mode 100644 tests/Unit/Http/Transformers/PartnerTransformerTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportExpiringSoonMailJobTest.php
create mode 100644 tests/Unit/Jobs/AutomatedReports/SendReportNotGeneratedMailJobTest.php
create mode 100644 tests/Unit/Listeners/Teams/SyncIntercomCompanyTest.php
create mode 100644 tests/Unit/Listeners/Users/SyncIntercomTest.php
create mode 100644 tests/Unit/Mail/Reports/ReportNotGeneratedTest.php
create mode 100644 tests/Unit/Models/PartnerTest.php
create mode 100644 tests/Unit/Services/ActivityServiceTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/RecordingOutcomeTextResolverTest.php
create mode 100644 tests/Unit/UseCases/TeamInsights/StrictConsentColumnResolverTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 628, done.
remote: Counting objects: 100% (331/331), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 628 (delta 280), reused 274 (delta 266), pack-reused 297 (from 1)
Receiving objects: 100% (628/628), 186.75 KiB | 1.23 MiB/s, done.
Resolving deltas: 100% (391/391), completed with 57 local objects.
From github.com:jiminny/app
ad2ce76737..12295204cf master -> origin/master
14f54b5be2..5e7646e5f9 JY-17836-participant-speeches-in-s3 -> origin/JY-17836-participant-speeches-in-s3
b167b19973..acba55cf38 JY-20289-api-tests -> origin/JY-20289-api-tests
f23cfee7c3..e5a3ec5dba JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null -> origin/JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
af59d60926..766efba1c5 JY-20395-fix-memory-issue-with-mail-import -> origin/JY-20395-fix-memory-issue-with-mail-import
* [new branch] JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
1737b7c528..c57e71e763 JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20671-anyvan-twilio-s3-recordings-ftech -> origin/JY-20671-anyvan-twilio-s3-recordings-ftech
+ ba181441c6...20a74137b0 JY-20742-mcp-poc -> origin/JY-20742-mcp-poc (forced update)
* [new branch] JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20816-calendar-events-diplication -> origin/JY-20816-calendar-events-diplication
* [new branch] desktop-app -> origin/desktop-app
4c4c974e46..185442c26e make-claude-great-again -> origin/make-claude-great-again
* [new branch] mcp-tools-schemas -> origin/mcp-tools-schemas
Updating ad2ce76737..12295204cf
Fast-forward
app/Component/Encoding/Job/AnalyzeTrackChannelsJob.php | 2 +-
app/Component/Transcription/TranscriptionProcessor/AssemblyAI/Services/SubmitAudioFileService.php | 66 ++----------------------------------------------------------------
app/Console/Commands/Tracks/CleanupActivityTracksCommand.php | 24 +++++++++++++++++-------
tests/Unit/Console/Commands/Tracks/CleanupActivityTracksCommandTest.php | 40 +++++++++++++++++++++++++++++-----------
tests/Unit/fixtures/assembly_ai_transcript_response.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_channel_diarization.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_error.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_not_ready.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_language_post.json | 1 -
tests/Unit/fixtures/assembly_ai_transcript_response_without_utterances.json | 1 -
11 files changed, 49 insertions(+), 90 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20818-move-AJ-reports-to-separated-datadog-metric
Switched to a new branch 'JY-20818-move-AJ-reports-to-separated-datadog-metric'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 83.615 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20818-move-AJ-reports-to-separated-datadog-metric) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5663/5663 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5663 files in 42.875 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 15 (delta 13), reused 15 (delta 13), pack-reused 0 (from 0)
Unpacking objects: 100% (15/15), 1.28 KiB | 72.00 KiB/s, done.
From github.com:jiminny/app
c57e71e763..8743fea32e JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20819-increase-download-transctip-rate-limit -> origin/JY-20819-increase-download-transctip-rate-limit
Already up to date.
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 180, done.
remote: Counting objects: 100% (179/179), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 180 (delta 127), reused 165 (delta 119), pack-reused 1 (from 1)
Receiving objects: 100% (180/180), 57.50 KiB | 2.74 MiB/s, done.
Resolving deltas: 100% (127/127), completed with 47 local objects.
From github.com:jiminny/app
12295204cf..35f036ace6 master -> origin/master
* [new branch] JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
5f21965da8..497fbc85c2 JY-20493-smart-instant-nudge-pre-filtering -> origin/JY-20493-smart-instant-nudge-pre-filtering
f029263118..6ed9e2bb13 JY-20808-low-priority-indexing-queue -> origin/JY-20808-low-priority-indexing-queue
* [new branch] JY-20817-fix-deleting-s3-directories -> origin/JY-20817-fix-deleting-s3-directories
* [new branch] JY-20820-es-reindex-stream-model-hydration -> origin/JY-20820-es-reindex-stream-model-hydration
+ ca92730ec5...2c8a1d0856 desktop-app -> origin/desktop-app (forced update)
8425d4e0de..89b45ec8b0 mcp-tools-schemas -> origin/mcp-tools-schemas
Updating 12295204cf..35f036ace6
Fast-forward
app/Component/Datadog/Constants.php | 1 +
app/Providers/RouteServiceProvider.php | 2 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackService.php | 13 +++++++++++++
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsCallbackServiceTest.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 62 insertions(+), 8 deletions(-)
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ gbr
* master
JY-20818-move-AJ-reports-to-separated-datadog-metric
JY-20773-fix-automated-reports-user-pilot-tracking
JY-20157-AJ-report-not-send-notification
JY-20508-notify-before-AJ-report-expiration
JY-20372-ai-reports-promotion-pages
JY-20352-sync-opportunities-without-a-local-owner-user-id-is-null
JY-20738-debug-AJ-tracking-UP
a
JY-18909-automated-reports-ask-jiminny
JY-20692-fix-integration-app-[API_KEY]
JY-20553-debug-crm-sync-delays
JY-20698-fix-SF-activity-types-on-new-playbook
JY-20543-AJ-report-tracking
JY-20384-handle-auto-sync-with-no-access-to-event-type
JY-20458-ask-jiminny-user-definitions
JY-19666-fix-import-contacts-account-association
JY-19666-HS-import-contacts-and-accounts-batch-job
JY-20458-Ask-Jiminny-Reports
JY-20200-batch-update-CRM-objects-Salesforce
JY-19666-HS-webhooks-add-contact-and-company
JY-20348-trigger-setup-DI-layout-on-team-creation
JY-20326-refactor-info-message-in-command
JY-20317-fix-auto-log-delay-issue-on-all-channels-disabled
JY-20312-remove-on-update-change-last-synced-at-crm-configurations
JY-20306-SF-skip-auto-sync-for-task-based-playbook
JY-20192-remove-deleted-team-from-saved-search-filters
JY-20197-import-opportunity-batch-job
JY-20293-enable-status-field-for-pipedrive-deals
JY-20191-remove-commands-interactive-prompts
JY-20118-change-default-sync-strategy
JY-20183-add-cache-on-auto-log-delay
JY-20197-add-import-opportunity-batch-job
20118-hs-opportunity-make-webhook-strategy-default
JY-20118-make-default-hs-opportunity-sync-strategy-webhook-based
JY-20196-handle-opportunity-without-note
JY-20118-improve-opportunity-import
JY-20189-handle-activity-search-on-deleted-groups
JY-20160
JY-20145-filter-out-converted-leads-when-matching
JY-20150-skip-push-summary-on-summary-ready-if-autolog
JY-20132-fix-note-encoding
JY-19792-clean-logs
JY-20117-fix-sync-profile-fields-on-empty-fields
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20725-handle-HS-search-rate-limit
Switched to a new branch 'JY-20725-handle-HS-search-rate-limit'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-emails:worker-emails_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend En...
|
20463
|
NULL
|
NULL
|
NULL
|
|
20503
|
889
|
5
|
2026-05-11T15:41:54.264061+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514114264_m2.jpg...
|
PhpStorm
|
faVsco.js – ClientTest.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIeWINavicatecodeFV faVsco.js?9 JY-20725-h PhostormVIeWINavicatecodeFV faVsco.js?9 JY-20725-handle-HS-search-rate-limiProiect v(c) MatchActivitvCrmData lest.ongRematchActivityOnCrmObjectDetach.php© HubspotPaginationService.phpAutoScoreD CrmDealRisksC) UserAutomatedReportscontroller.ong© Hubspot/Service.phpOhubspot/service.pnpT SvncCrmEntities Trait.onp© JiminnyDebugCommand.phg>D ElasticSearch© MatchActivityCrmData.phg© RateLimitException.phpboroups>D Import>@ Mailbox|> @ Opportunities© PaginationState.php© PaginationConfia.phpw Playlists>CJ Teamsclass Cllentrest extends Testcaseprivate Client $client;8 usagesprivate Configuration $config;a lranscription•JUsers> D Webhook07 Mail* @vac SocialAccountService&Mock0bjectModelsNotiticationsprivace soc1alAccountservice ssoc1alAccouncserv1cenock™b ObserversPolicies• ProvidersRenositories)* dvar HubspotraqinatzonservicexmockubnectSholfLocal ChangesConsoleXLog xChanaes 8 fles+ → E Side-by-side viewer -Do not ignore8 02d5214b app/Console/Commands/JiminnyDebuqCommand.phgHighlight wordsXBB ?=.env.local app@ Client.pho anp/Services/Crm/Hubspot© ClientTest.php tests/Unit/Services/Crm/Hubspot© HandleHubspotRateLimitTest.php tests/Unit/Jobs/Middleware© JiminnyDebugCommand.php app/Console/Commandsnamespace Jiminnv Console Commands:pip loeeine.one contiel© MatchActivityCrmData.php app/Jobs/CrmRateLimitException.php app/ExceptionsUnversioned Files 9 files.env.nikilocal appE.env.other app@ CanAccessAiReportsTest.php tests/Unit/Policies© CreateMockAskJiminnyReportResultCommand.php app/Console/Commands/Re( favicon.ico publicE ids.txt appBraw sal_querv.sal app© SimulateWebhooksCommand.php app/Console/Commands/Crm/Hubspoluse Iluminate Console Commands* Class Jiminnudebuacommand* anackade minnu console commandsiclass JiminnyDebugCommand extends Commandprotected Ssignature = 'jiminny:debug';M.WE8TOOK FILTERING IMPLEMENTATION.mo a00public function handle: void$this->line('this is a debug tool'):ovi+(1).C) TrackAutomatedReportGeneratedevent.onpC) CachedCrmServiceDecorator.ongCheскAnакetrукemotematch.pngC) Kernel.phpA19 A144 M11 ^100% MOn 11 May 18.41:00U ClientTest vA SF [jiminny@localhost]4 HS_local [jiminny@localhost]A console [PROD]« console (EU]A console [STAGING]"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEs0kXi ~07-May-26 14:51:15 GMT: domain=.hubapi.com: Http0nly: Secure: SameSite=None"].V.19AN"urL\":1"https:(VNVa.nel.cloudfLare.com/V/reportiV/v4?s=NYALsVTPotYm52qrSDJxYE4sd2RWRq15p5wHsmd=g<Lz@YdxLx2B1XVpHmsKn50%2BKVA5mF1J2m/YRECD65nx2BW2LY1206FM14%2l M("group"; \"cf-nell","max age":604800,"J,"success traction".0.olg"max ade":6048002""Serven":"cloudflare"?>4"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab","trace id"."c7ab8365-903f-46d4-9403-0e5b551e3545"}6 differences»current versionnamespace Jaminnv console Commands.use carbon Carbon:use carbon Carbonimmutable:use Illuminate\ Console\ Command:use ILuminate Support Facades Redis:use InvalidAraumentExcent.ionuse Jiminny..lohs AutomatedRenorts.RequestGenerate.sk.aminnvRenont.lob:use Jiminny Jobs\AutomatedReports\SendReportMailJob;use Jiminny Jobs\Crm \Delete\VerifyActivityCrmTaskJob;use Jiminny Jobs\Crm \MatchActivityCrmDatause Jiminnv Jlohs .lohDisnatcherIntenface.use Jiminny\Models\Activity;use Jiminny Models\AutomatedReport;use Jiminny Models\AutomatedReportResult;use Jiminny\Models\Teamuse Jiminny Models Useruse Jiminny Repositories AutomatedReportsRepository:use Jiminny Services Activity\ Crm0wnerResolveruce _liminnv|Sonvicoc|KiocklAutomatedPenontc|AutomatodPonontcSonvico»use Jiminny services Userrilot Userrilotullent* Class JiminnuDebuaCommandfo 4 spaces...
|
NULL
|
-6071083337438375197
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIeWINavicatecodeFV faVsco.js?9 JY-20725-h PhostormVIeWINavicatecodeFV faVsco.js?9 JY-20725-handle-HS-search-rate-limiProiect v(c) MatchActivitvCrmData lest.ongRematchActivityOnCrmObjectDetach.php© HubspotPaginationService.phpAutoScoreD CrmDealRisksC) UserAutomatedReportscontroller.ong© Hubspot/Service.phpOhubspot/service.pnpT SvncCrmEntities Trait.onp© JiminnyDebugCommand.phg>D ElasticSearch© MatchActivityCrmData.phg© RateLimitException.phpboroups>D Import>@ Mailbox|> @ Opportunities© PaginationState.php© PaginationConfia.phpw Playlists>CJ Teamsclass Cllentrest extends Testcaseprivate Client $client;8 usagesprivate Configuration $config;a lranscription•JUsers> D Webhook07 Mail* @vac SocialAccountService&Mock0bjectModelsNotiticationsprivace soc1alAccountservice ssoc1alAccouncserv1cenock™b ObserversPolicies• ProvidersRenositories)* dvar HubspotraqinatzonservicexmockubnectSholfLocal ChangesConsoleXLog xChanaes 8 fles+ → E Side-by-side viewer -Do not ignore8 02d5214b app/Console/Commands/JiminnyDebuqCommand.phgHighlight wordsXBB ?=.env.local app@ Client.pho anp/Services/Crm/Hubspot© ClientTest.php tests/Unit/Services/Crm/Hubspot© HandleHubspotRateLimitTest.php tests/Unit/Jobs/Middleware© JiminnyDebugCommand.php app/Console/Commandsnamespace Jiminnv Console Commands:pip loeeine.one contiel© MatchActivityCrmData.php app/Jobs/CrmRateLimitException.php app/ExceptionsUnversioned Files 9 files.env.nikilocal appE.env.other app@ CanAccessAiReportsTest.php tests/Unit/Policies© CreateMockAskJiminnyReportResultCommand.php app/Console/Commands/Re( favicon.ico publicE ids.txt appBraw sal_querv.sal app© SimulateWebhooksCommand.php app/Console/Commands/Crm/Hubspoluse Iluminate Console Commands* Class Jiminnudebuacommand* anackade minnu console commandsiclass JiminnyDebugCommand extends Commandprotected Ssignature = 'jiminny:debug';M.WE8TOOK FILTERING IMPLEMENTATION.mo a00public function handle: void$this->line('this is a debug tool'):ovi+(1).C) TrackAutomatedReportGeneratedevent.onpC) CachedCrmServiceDecorator.ongCheскAnакetrукemotematch.pngC) Kernel.phpA19 A144 M11 ^100% MOn 11 May 18.41:00U ClientTest vA SF [jiminny@localhost]4 HS_local [jiminny@localhost]A console [PROD]« console (EU]A console [STAGING]"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEs0kXi ~07-May-26 14:51:15 GMT: domain=.hubapi.com: Http0nly: Secure: SameSite=None"].V.19AN"urL\":1"https:(VNVa.nel.cloudfLare.com/V/reportiV/v4?s=NYALsVTPotYm52qrSDJxYE4sd2RWRq15p5wHsmd=g<Lz@YdxLx2B1XVpHmsKn50%2BKVA5mF1J2m/YRECD65nx2BW2LY1206FM14%2l M("group"; \"cf-nell","max age":604800,"J,"success traction".0.olg"max ade":6048002""Serven":"cloudflare"?>4"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab","trace id"."c7ab8365-903f-46d4-9403-0e5b551e3545"}6 differences»current versionnamespace Jaminnv console Commands.use carbon Carbon:use carbon Carbonimmutable:use Illuminate\ Console\ Command:use ILuminate Support Facades Redis:use InvalidAraumentExcent.ionuse Jiminny..lohs AutomatedRenorts.RequestGenerate.sk.aminnvRenont.lob:use Jiminny Jobs\AutomatedReports\SendReportMailJob;use Jiminny Jobs\Crm \Delete\VerifyActivityCrmTaskJob;use Jiminny Jobs\Crm \MatchActivityCrmDatause Jiminnv Jlohs .lohDisnatcherIntenface.use Jiminny\Models\Activity;use Jiminny Models\AutomatedReport;use Jiminny Models\AutomatedReportResult;use Jiminny\Models\Teamuse Jiminny Models Useruse Jiminny Repositories AutomatedReportsRepository:use Jiminny Services Activity\ Crm0wnerResolveruce _liminnv|Sonvicoc|KiocklAutomatedPenontc|AutomatodPonontcSonvico»use Jiminny services Userrilot Userrilotullent* Class JiminnuDebuaCommandfo 4 spaces...
|
20463
|
NULL
|
NULL
|
NULL
|
|
20505
|
889
|
6
|
2026-05-11T15:41:58.025956+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514118025_m2.jpg...
|
PhpStorm
|
faVsco.js – ClientTest.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavigareCodeLaravelKeractorWindowmelp PhostormVIewINavigareCodeLaravelKeractorWindowmelpFV faVsco.js?9 JY-20725-handle-HS-search-rate-limitProiectRematchActivityOnCrmObjectDetach.php© HubspotPaginationService.php(C) TrackAutomatedReportGenerateaevent.onpAutoScoreD CrmDealRisksC) UserAutomatedReportscontroller.ongOhuospot/service.pnpOhubspot/service.pnp© SyncCrmEntitiesTrait.phpC) CachedCrmServiceDecorator.ong© CheckAndRetryRemoteMatch.php• D ElasticSearchb Groups>D Import>@ Mailbox> @ Opportunities© MatchActivityCrmData.phg© RateLimitException.phpC) HandlerubspotkateLimit.php©) ClientTest.php x © Kernel.php© PaginationState.phpw Playlists>CJ Teamsclass Cllentrest extends Testcaseprivate Client $client;8 usagesprivate Configuration $config;m A19 A144 X11 ^a lranscription•JUsers* @vac SocialAccountService&Mock0bjectwedhook07 MailModelsNotiticationsprivace soc1alAccountservice ssoc1alAccouncserv1cenock™b ObserversPolicies• ProvidersRenositories* dvar HubspotraqinatzonservicexmockubnectSholfLocal ChangesConsoleXLog xChanaes 8 fles+ → a Side-by-side viewer +8 02d5214b app/Jobs/Crm/MatchActivityCrmData.phpDo not ignoreHighlight wordsXBB ?=.env.local app@ Client.pho anp/Services/Crm/Hubspot© ClientTest.php tests/Unit/Services/Crm/Hubspot© HandleHubspotRateLimitTest.php tests/Unit/Jobs/Middleware© JiminnyDebugCommand.php app/Console/Commandsphp logging.php config© MatchActivityCrmData.php app/Jobs/CrmRateLimitException.php app/ExceptionsUnversioned Files 9 files.env.nikilocal app= env.other app@ CanAccessAiReportsTest.php tests/Unit/Policies© CreateMockAskJiminnyReportResultCommand.php app/Console/Commands/Re( favicon.ico publicE ids.txt appBraw sal_querv.sal app© SimulateWebhooksCommand.php app/Console/Commands/Crm/Hubspot'exception' => sexception->qetMessaqeo.'attempts = sthis->attemptso.if (Sexcention instanceof RateLimitException ll Sexcention instanceof \Illuminate\Queue\MaxAttemotsExceededException) ≤Loq::warnina('MatchActivitvcrmbatal Joolpermanently failed due to rate limiting'. Scontext)'}else {Log: :error('[MatchActivityCrmData) Job permanently failed after all retries', Scontext);M.WE8TOOK FILTERING IMPLEMENTATION.mo a00Tacts naccod: 80 (10 minutes acolhhl100% C47 • Mon 11 May 18:41:57ClientTestA SF [jiminny@localhost]4 HS_local [jiminny@localhost]A console [PROD]A console [EUiconsole [STAGINGI"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWM0Q.UfZEXDZyHz2mBUFdzdo2gTHEsOkX1 ~07-May-26 14:51:15 GMT: domain=.hubapi.com: Http0nly: Secure: SameSite=None"]."кeрoгс-1о"."?"url": "http:V/AWa.nel.cloudfLare.com/V/report|V/v4?s=NYALsVTP0fYm52qrsDgxYE4sd2RwRq15p5wHsmd=g<LZOYdxLx2B1XVpHmsKnS0%2BKVA5mFLJ2m/YRECD65Ho2BW2LYT206FM4%2lm"max age":604800*"]"success traction".0.olg"max age":6048002""Serven":"cloudflare"?>4"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",1 differenceTeirront vorcian'exception' => $exception->getMessage()'attempts = sthis->attemptso.// RateLimitException reaches failed() only when retryUntil() expires while the job is// still held in the "released" state by HandleHubspotRateLimit - the middleware neven// re-throws, so the exception is not the direct cause of the failure in normal flowif (Sexcention instanceof RateLimitException ll Sexcention instanceof \Tlluminate\Queue\MaxAttemotsExceededException) ≤Loa::warnina('[MatchActivitvCrmDatal Job permanently failed due to rate Limiting'. Scontext):}else {Log::error('[MatchActivityCrmData] Job permanently failed after all retries', Scontext);W Windsurf Teams 61:8 UTF-8 P 4 spaces ®...
|
NULL
|
8235358747761985773
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhostormVIewINavigareCodeLaravelKeractorWindowmelp PhostormVIewINavigareCodeLaravelKeractorWindowmelpFV faVsco.js?9 JY-20725-handle-HS-search-rate-limitProiectRematchActivityOnCrmObjectDetach.php© HubspotPaginationService.php(C) TrackAutomatedReportGenerateaevent.onpAutoScoreD CrmDealRisksC) UserAutomatedReportscontroller.ongOhuospot/service.pnpOhubspot/service.pnp© SyncCrmEntitiesTrait.phpC) CachedCrmServiceDecorator.ong© CheckAndRetryRemoteMatch.php• D ElasticSearchb Groups>D Import>@ Mailbox> @ Opportunities© MatchActivityCrmData.phg© RateLimitException.phpC) HandlerubspotkateLimit.php©) ClientTest.php x © Kernel.php© PaginationState.phpw Playlists>CJ Teamsclass Cllentrest extends Testcaseprivate Client $client;8 usagesprivate Configuration $config;m A19 A144 X11 ^a lranscription•JUsers* @vac SocialAccountService&Mock0bjectwedhook07 MailModelsNotiticationsprivace soc1alAccountservice ssoc1alAccouncserv1cenock™b ObserversPolicies• ProvidersRenositories* dvar HubspotraqinatzonservicexmockubnectSholfLocal ChangesConsoleXLog xChanaes 8 fles+ → a Side-by-side viewer +8 02d5214b app/Jobs/Crm/MatchActivityCrmData.phpDo not ignoreHighlight wordsXBB ?=.env.local app@ Client.pho anp/Services/Crm/Hubspot© ClientTest.php tests/Unit/Services/Crm/Hubspot© HandleHubspotRateLimitTest.php tests/Unit/Jobs/Middleware© JiminnyDebugCommand.php app/Console/Commandsphp logging.php config© MatchActivityCrmData.php app/Jobs/CrmRateLimitException.php app/ExceptionsUnversioned Files 9 files.env.nikilocal app= env.other app@ CanAccessAiReportsTest.php tests/Unit/Policies© CreateMockAskJiminnyReportResultCommand.php app/Console/Commands/Re( favicon.ico publicE ids.txt appBraw sal_querv.sal app© SimulateWebhooksCommand.php app/Console/Commands/Crm/Hubspot'exception' => sexception->qetMessaqeo.'attempts = sthis->attemptso.if (Sexcention instanceof RateLimitException ll Sexcention instanceof \Illuminate\Queue\MaxAttemotsExceededException) ≤Loq::warnina('MatchActivitvcrmbatal Joolpermanently failed due to rate limiting'. Scontext)'}else {Log: :error('[MatchActivityCrmData) Job permanently failed after all retries', Scontext);M.WE8TOOK FILTERING IMPLEMENTATION.mo a00Tacts naccod: 80 (10 minutes acolhhl100% C47 • Mon 11 May 18:41:57ClientTestA SF [jiminny@localhost]4 HS_local [jiminny@localhost]A console [PROD]A console [EUiconsole [STAGINGI"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWM0Q.UfZEXDZyHz2mBUFdzdo2gTHEsOkX1 ~07-May-26 14:51:15 GMT: domain=.hubapi.com: Http0nly: Secure: SameSite=None"]."кeрoгс-1о"."?"url": "http:V/AWa.nel.cloudfLare.com/V/report|V/v4?s=NYALsVTP0fYm52qrsDgxYE4sd2RwRq15p5wHsmd=g<LZOYdxLx2B1XVpHmsKnS0%2BKVA5mFLJ2m/YRECD65Ho2BW2LYT206FM4%2lm"max age":604800*"]"success traction".0.olg"max age":6048002""Serven":"cloudflare"?>4"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",1 differenceTeirront vorcian'exception' => $exception->getMessage()'attempts = sthis->attemptso.// RateLimitException reaches failed() only when retryUntil() expires while the job is// still held in the "released" state by HandleHubspotRateLimit - the middleware neven// re-throws, so the exception is not the direct cause of the failure in normal flowif (Sexcention instanceof RateLimitException ll Sexcention instanceof \Tlluminate\Queue\MaxAttemotsExceededException) ≤Loa::warnina('[MatchActivitvCrmDatal Job permanently failed due to rate Limiting'. Scontext):}else {Log::error('[MatchActivityCrmData] Job permanently failed after all retries', Scontext);W Windsurf Teams 61:8 UTF-8 P 4 spaces ®...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
20506
|
889
|
7
|
2026-05-11T15:42:00.999784+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514120999_m2.jpg...
|
PhpStorm
|
faVsco.js – ClientTest.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
[{"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-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.8597075,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"ClientTest","depth":6,"bounds":{"left":0.875,"top":0.019952115,"width":0.04055851,"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 'ClientTest'","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 'ClientTest'","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":"19","depth":4,"bounds":{"left":0.50265956,"top":0.17478053,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"144","depth":4,"bounds":{"left":0.5142952,"top":0.17478053,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"11","depth":4,"bounds":{"left":0.52825797,"top":0.17478053,"width":0.008976064,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.53889626,"top":0.17318435,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.5462101,"top":0.17318435,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.96276593,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4682894321548166980
|
4446428687123393012
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
20505
|
NULL
|
NULL
|
NULL
|
|
20509
|
889
|
8
|
2026-05-11T15:42:52.469740+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514172469_m2.jpg...
|
PhpStorm
|
faVsco.js – ClientTest.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
[{"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-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.8597075,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"ClientTest","depth":6,"bounds":{"left":0.875,"top":0.019952115,"width":0.04055851,"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 'ClientTest'","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 'ClientTest'","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":"19","depth":4,"bounds":{"left":0.50265956,"top":0.17478053,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"144","depth":4,"bounds":{"left":0.5142952,"top":0.17478053,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"11","depth":4,"bounds":{"left":0.52825797,"top":0.17478053,"width":0.008976064,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.53889626,"top":0.17318435,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.5462101,"top":0.17318435,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.96276593,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4682894321548166980
|
4446428687123393012
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
20511
|
889
|
9
|
2026-05-11T15:43:22.834044+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514202834_m2.jpg...
|
PhpStorm
|
faVsco.js – ClientTest.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
[{"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-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.8597075,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"ClientTest","depth":6,"bounds":{"left":0.875,"top":0.019952115,"width":0.04055851,"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 'ClientTest'","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 'ClientTest'","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":"19","depth":4,"bounds":{"left":0.50265956,"top":0.17478053,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"144","depth":4,"bounds":{"left":0.5142952,"top":0.17478053,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"11","depth":4,"bounds":{"left":0.52825797,"top":0.17478053,"width":0.008976064,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.53889626,"top":0.17318435,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.5462101,"top":0.17318435,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.96276593,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4682894321548166980
|
4446428687123393012
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
20509
|
NULL
|
NULL
|
NULL
|
|
20513
|
889
|
10
|
2026-05-11T15:43:53.207643+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514233207_m2.jpg...
|
PhpStorm
|
faVsco.js – ClientTest.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
[{"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-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.8597075,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"ClientTest","depth":6,"bounds":{"left":0.875,"top":0.019952115,"width":0.04055851,"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 'ClientTest'","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 'ClientTest'","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":"19","depth":4,"bounds":{"left":0.50265956,"top":0.17478053,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"144","depth":4,"bounds":{"left":0.5142952,"top":0.17478053,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"11","depth":4,"bounds":{"left":0.52825797,"top":0.17478053,"width":0.008976064,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.53889626,"top":0.17318435,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.5462101,"top":0.17318435,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.96276593,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4682894321548166980
|
4446428687123393012
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
20509
|
NULL
|
NULL
|
NULL
|
|
20518
|
890
|
0
|
2026-05-11T15:45:03.409811+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514303409_m1.jpg...
|
PhpStorm
|
faVsco.js – ClientTest.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
[{"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-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"ClientTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'ClientTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'ClientTest'","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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"144","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"11","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\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","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":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
4682894321548166980
|
4446428687123393012
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
20508
|
NULL
|
NULL
|
NULL
|
|
20520
|
890
|
1
|
2026-05-11T15:45:33.762155+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514333762_m1.jpg...
|
PhpStorm
|
faVsco.js – ClientTest.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
[{"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-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"ClientTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'ClientTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'ClientTest'","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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"144","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"11","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\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","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":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
4682894321548166980
|
4446428687123393012
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
20508
|
NULL
|
NULL
|
NULL
|
|
20522
|
890
|
2
|
2026-05-11T15:46:04.085829+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514364085_m1.jpg...
|
PhpStorm
|
faVsco.js – ClientTest.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
[{"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-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"ClientTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'ClientTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'ClientTest'","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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"144","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"11","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\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Hubspot;\n\nuse GuzzleHttp\\Psr7\\Response;\nuse HubSpot\\Client\\Crm\\Associations\\Api\\BatchApi;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti;\nuse HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi as DealsBasicApi;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Api\\PipelinesApi;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\CollectionResponsePipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Pipeline;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Api\\CoreApi;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery as HubSpotDiscovery;\nuse HubSpot\\Discovery\\Crm\\Deals\\Discovery as DealsDiscovery;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotTokenManager;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Jiminny\\Services\\SocialAccountService;\nuse League\\OAuth2\\Client\\Token\\AccessToken;\nuse Mockery;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\NullLogger;\nuse SevenShores\\Hubspot\\Endpoints\\Engagements;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Client as HubspotClient;\nuse SevenShores\\Hubspot\\Http\\Response as HubspotResponse;\n\n/**\n * @runTestsInSeparateProcesses\n *\n * @preserveGlobalState disabled\n */\nclass ClientTest extends TestCase\n{\n private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';\n private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';\n private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';\n /**\n * @var Client&MockObject\n */\n private Client $client;\n private Configuration $config;\n\n /**\n * @var SocialAccountService&MockObject\n */\n private SocialAccountService $socialAccountServiceMock;\n\n /**\n * @var HubspotPaginationService&MockObject\n */\n private HubspotPaginationService $paginationServiceMock;\n\n /**\n * @var HubspotTokenManager&MockObject\n */\n private HubspotTokenManager $tokenManagerMock;\n\n /**\n * @var CoreApi&MockObject\n */\n private CoreApi $coreApiMock;\n\n /**\n * @var PipelinesApi&MockObject\n */\n private PipelinesApi $pipelinesApiMock;\n\n /**\n * @var BatchApi&MockObject\n */\n private BatchApi $associationsBatchApiMock;\n\n /**\n * @var DealsBasicApi&MockObject\n */\n private DealsBasicApi $dealsBasicApiMock;\n\n /**\n * @var Engagements&MockObject\n */\n private $engagementsMock;\n\n /**\n * @var HubspotClient&MockObject\n */\n private HubspotClient $hubspotClientMock;\n\n protected function setUp(): void\n {\n // Create mocks for dependencies\n $this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);\n $this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);\n $this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);\n\n // Create a real Client instance with mocked dependencies\n // Create a partial mock only for the methods we need to mock\n $client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);\n $client->setLogger(new NullLogger());\n\n // Inject the real dependencies using reflection\n $reflection = new \\ReflectionClass(Client::class);\n\n $accountServiceProperty = $reflection->getProperty('accountService');\n $accountServiceProperty->setAccessible(true);\n $accountServiceProperty->setValue($client, $this->socialAccountServiceMock);\n\n $paginationServiceProperty = $reflection->getProperty('paginationService');\n $paginationServiceProperty->setAccessible(true);\n $paginationServiceProperty->setValue($client, $this->paginationServiceMock);\n\n $tokenManagerProperty = $reflection->getProperty('tokenManager');\n $tokenManagerProperty->setAccessible(true);\n $tokenManagerProperty->setValue($client, $this->tokenManagerMock);\n $factoryMock = $this->createMock(Factory::class);\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $engagementsMock = $this->createMock(\\SevenShores\\Hubspot\\Endpoints\\Engagements::class);\n\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n $factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);\n\n $client->method('getInstance')->willReturn($factoryMock);\n\n $discoveryMock = $this->createMock(HubSpotDiscovery\\Discovery::class);\n $crmMock = $this->createMock(HubSpotDiscovery\\Crm\\Discovery::class);\n $associationsMock = $this->createMock(HubSpotDiscovery\\Crm\\Associations\\Discovery::class);\n $propertiesMock = $this->createMock(HubSpotDiscovery\\Crm\\Properties\\Discovery::class);\n $pipelinesMock = $this->createMock(HubSpotDiscovery\\Crm\\Pipelines\\Discovery::class);\n $coreApiMock = $this->createMock(CoreApi::class);\n $pipelinesApiMock = $this->createMock(PipelinesApi::class);\n $associationsBatchApiMock = $this->createMock(BatchApi::class);\n $dealsBasicApiMock = $this->createMock(DealsBasicApi::class);\n\n $propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);\n $pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);\n $associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);\n $dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);\n $dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);\n\n $returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];\n $crmMock->method('__call')\n ->willReturnCallback(static fn (string $name) => $returnMap[$name])\n ;\n\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n\n $this->client = $client;\n $this->config = $this->createMock(Configuration::class);\n $this->client->setConfiguration($this->config);\n $this->coreApiMock = $coreApiMock;\n $this->pipelinesApiMock = $pipelinesApiMock;\n $this->associationsBatchApiMock = $associationsBatchApiMock;\n $this->dealsBasicApiMock = $dealsBasicApiMock;\n $this->engagementsMock = $engagementsMock;\n $this->hubspotClientMock = $hubspotClientMock;\n }\n\n public function testGetMinimumApiVersion(): void\n {\n $this->assertIsString($this->client->getMinimumApiVersion());\n }\n\n public function testGetInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setBaseUrl('https://example.com');\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n $factory = $client->getInstance();\n\n $this->assertEquals('foo', $factory->getClient()->key);\n }\n\n public function testGetNewInstance(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $client = new Client($socialAccountService, $paginationService, $tokenManager);\n $client->setAccessToken(new AccessToken(['access_token' => 'foo']));\n\n $factory = $client->getNewInstance();\n $token = $factory->auth()->oAuth()->accessTokensApi()->getConfig()->getAccessToken();\n $this->assertEquals('foo', $token);\n }\n\n public function testFetchProperty(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n 'some_property',\n $this->client->fetchProperty('foo', 'test')['name']\n );\n }\n\n public function testFetchPropertyOptions(): void\n {\n $this->coreApiMock->method('getByName')->willReturn($this->generateProperty());\n\n $this->assertEquals(\n [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n $this->client->fetchPropertyOptions('foo', 'test')\n );\n }\n\n public function testFetchCallDispositions(): void\n {\n $this->engagementsMock\n ->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse([\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ]))\n ;\n\n $this->assertEquals(\n [\n [\n 'id' => 1,\n 'label' => 'foo',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'bar',\n 'deleted' => false,\n ],\n ],\n $this->client->fetchCallDispositions()\n );\n }\n\n public function testFetchDispositionFieldOptions(): void\n {\n $this->engagementsMock->method('getCallDispositions')\n ->willReturn($this->generateHubSpotResponse(\n [\n [\n 'id' => 1,\n 'label' => 'Label 1',\n 'deleted' => false,\n ],\n [\n 'id' => 2,\n 'label' => 'Label 2',\n 'deleted' => true,\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n [\n 'value' => 1,\n 'id' => 1,\n 'label' => 'Label 1',\n ],\n ],\n $this->client->fetchDispositionFieldOptions()\n );\n }\n\n public function testFetchOpportunityPipelineStages(): void\n {\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n $this->client->fetchOpportunityPipelineStages()\n );\n }\n\n public function testFetchOpportunityPipelineStagesErrorResponse(): void\n {\n $this->pipelinesApiMock\n ->method('getAll')\n ->with('deals')\n ->willReturn(new Error(['message' => 'test error']))\n ;\n\n $this->assertEmpty($this->client->fetchOpportunityPipelineStages());\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('meetingOutcomeFieldProvider')]\n public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void\n {\n $field = new Field(['crm_provider_id' => $fieldId]);\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('GET', $expectedEndpoint)\n ->willReturn($this->generateHubSpotResponse(\n [\n 'options' => [\n ['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]\n ))\n ;\n\n $this->assertEquals(\n [\n ['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],\n ['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],\n ['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],\n ],\n $this->client->fetchMeetingOutcomeFieldOptions($field)\n );\n }\n\n public static function meetingOutcomeFieldProvider(): array\n {\n return [\n 'meeting outcome field' => [\n 'meetingOutcome',\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome',\n ],\n 'activity type field' => [\n 'foobar',\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type',\n ],\n ];\n }\n\n #[\\PHPUnit\\Framework\\Attributes\\DataProvider('opportunityFieldOptionsProvider')]\n public function testFetchOpportunityFieldOptions(Field $field, string $type): void\n {\n $responses = [\n self::RESPONSE_TYPE_STAGE_FIELD => [\n ['id' => 'foo', 'label' => 'bar'],\n ['id' => 'baz', 'label' => 'qux'],\n ],\n self::RESPONSE_TYPE_PIPELINE_FIELD => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n self::RESPONSE_TYPE_REGULAR_FIELD => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ];\n\n $field = $this->createMock(Field::class);\n\n if ($type === self::RESPONSE_TYPE_REGULAR_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(false);\n $propertyResponse = $this->generateProperty();\n $this->coreApiMock->method('getByName')->willReturn($propertyResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_STAGE_FIELD) {\n $field->method('isStageField')->willReturn(true);\n /**\n * @TODO: The class CollectionResponsePipeline will be deprecated in the next Hubspot version\n */\n\n $pipelineStagesResponse = new CollectionResponsePipeline([\n 'results' => [$this->generatePipeline()],\n ]);\n $this->pipelinesApiMock\n ->method('getAll')\n ->willReturn($pipelineStagesResponse);\n }\n\n if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {\n $field->method('isStageField')->willReturn(false);\n $field->method('isPipelineField')->willReturn(true);\n $pipelineResponse = $this->generateHubSpotResponse([\n 'results' => [\n ['id' => '123', 'label' => 'Sales'],\n ['id' => 'default', 'label' => 'CS'],\n ],\n ]);\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($pipelineResponse);\n }\n\n $this->assertEquals(\n $responses[$type],\n $this->client->fetchOpportunityFieldOptions($field)\n );\n }\n\n public static function opportunityFieldOptionsProvider(): array\n {\n return [\n 'stage field' => [\n 'field' => new Field(['crm_provider_id' => 'dealstage']),\n 'type' => self::RESPONSE_TYPE_STAGE_FIELD,\n ],\n 'pipeline field' => [\n 'field' => new Field(['crm_provider_id' => 'pipeline']),\n 'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,\n ],\n 'regular field' => [\n 'field' => new Field(['crm_provider_id' => 'some_property']),\n 'type' => self::RESPONSE_TYPE_REGULAR_FIELD,\n ],\n ];\n }\n\n private function generateHubSpotResponse(array $data): HubspotResponse\n {\n return new HubspotResponse(new Response(200, [], json_encode($data)));\n }\n\n private function generateProperty(): Property\n {\n return new Property([\n 'name' => 'some_property',\n 'options' => [\n [\n 'label' => 'label_1',\n 'value' => 'value_1',\n ],\n [\n 'label' => 'label_2',\n 'value' => 'value_2',\n ],\n ],\n ]);\n }\n\n private function generatePipeline(): Pipeline\n {\n return new Pipeline(['stages' => [\n new PipelineStage(['id' => 'foo', 'label' => 'bar']),\n new PipelineStage(['id' => 'baz', 'label' => 'qux']),\n ]]);\n }\n\n public function testFetchOpportunityPipelines(): void\n {\n $this->client\n ->method('makeRequest')\n ->with('/crm/v3/pipelines/deals')\n ->willReturn($this->generateHubSpotResponse([\n 'results' => [\n ['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],\n ['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],\n ['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],\n ],\n ]));\n\n $this->assertEquals(\n [\n ['id' => 'id_1', 'label' => 'Option 1'],\n ['id' => 'id_2', 'label' => 'Option 2'],\n ['id' => 'id_3', 'label' => 'Option 3'],\n ],\n $this->client->fetchOpportunityPipelines()\n );\n }\n\n public function testGetPaginatedData(): void\n {\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ['id' => 'id_3', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator and modify reference parameters\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n ['payload_key' => 'payload_value'],\n 'foobar',\n 0,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {\n $total = 3;\n $lastRecordId = 'id_3';\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n $this->assertEquals(\n [\n 'results' => $expectedResults,\n 'total' => 3,\n 'last_record' => 'id_3',\n ],\n $this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')\n );\n }\n\n public function testGetAssociationsData(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $responseResults = [];\n foreach ($ids as $id) {\n $from = new PublicObjectId();\n $from->setId($id);\n\n $to1 = new PublicObjectId();\n $to1->setId('contact_' . $id . '_1');\n $to2 = new PublicObjectId();\n $to2->setId('contact_' . $id . '_2');\n\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to1, $to2]);\n\n $responseResults[] = $result;\n }\n\n $batchResponse = new BatchResponsePublicAssociationMulti();\n $batchResponse->setResults($responseResults);\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {\n $inputIds = array_map(\n fn ($input) => $input->getId(),\n $batchInput->getInputs()\n );\n\n return $inputIds === $ids;\n })\n )\n ->willReturn($batchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $expectedResult = [\n '1' => ['contact_1_1', 'contact_1_2'],\n '2' => ['contact_2_1', 'contact_2_2'],\n '3' => ['contact_3_1', 'contact_3_2'],\n ];\n\n $this->assertEquals($expectedResult, $result);\n }\n\n public function testGetAssociationsDataHandlesException(): void\n {\n $ids = ['1', '2', '3'];\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $exception = new \\Exception('API Error');\n\n $this->associationsBatchApiMock->expects($this->once())\n ->method('read')\n ->with(\n $this->equalTo($fromObject),\n $this->equalTo($toObject),\n $this->callback(function ($batchInput) use ($ids) {\n return $batchInput instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId\n && count($batchInput->getInputs()) === count($ids);\n })\n )\n ->willThrowException($exception);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[Hubspot] Failed to fetch associations',\n [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => 'API Error',\n ]\n );\n\n $this->client->setLogger($loggerMock);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertEmpty($result);\n $this->assertIsArray($result);\n }\n\n public function testGetAssociationsDataWithLargeDataSet(): void\n {\n $ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items\n $fromObject = 'deals';\n $toObject = 'contacts';\n\n $firstBatchResponse = new BatchResponsePublicAssociationMulti();\n $firstBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $firstBatchResponse->setResults($firstBatchResults);\n\n $secondBatchResponse = new BatchResponsePublicAssociationMulti();\n $secondBatchResults = array_map(function ($id) {\n $from = new PublicObjectId();\n $from->setId($id);\n $to = new PublicObjectId();\n $to->setId('contact_' . $id);\n $result = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicAssociationMulti();\n $result->setFrom($from);\n $result->setTo([$to]);\n\n return $result;\n }, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));\n $secondBatchResponse->setResults($secondBatchResults);\n\n $this->associationsBatchApiMock->expects($this->exactly(3))\n ->method('read')\n ->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);\n\n $result = $this->client->getAssociationsData($ids, $fromObject, $toObject);\n\n $this->assertCount(2500, $result);\n $this->assertArrayHasKey('1', $result);\n $this->assertArrayHasKey('2500', $result);\n $this->assertEquals(['contact_1'], $result['1']);\n $this->assertEquals(['contact_2500'], $result['2500']);\n }\n\n public function testGetContactByEmailSuccess(): void\n {\n $email = 'test@example.com';\n $fields = ['firstname', 'lastname', 'email'];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn([\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ]);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname,email', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n 'email' => 'test@example.com',\n ],\n ], $result);\n }\n\n public function testGetContactByEmailWithEmptyFields(): void\n {\n $email = 'test@example.com';\n $fields = [];\n\n $contactMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $contactMock->method('getId')->willReturn('12345');\n $contactMock->method('getProperties')->willReturn(['email' => 'test@example.com']);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, '', null, false, 'email')\n ->willReturn($contactMock);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new NullLogger());\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([\n 'id' => '12345',\n 'properties' => ['email' => 'test@example.com'],\n ], $result);\n }\n\n public function testGetContactByEmailApiException(): void\n {\n $email = 'nonexistent@example.com';\n $fields = ['firstname', 'lastname'];\n\n $exception = new \\HubSpot\\Client\\Crm\\Contacts\\ApiException('Contact not found', 404);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($email, 'firstname,lastname', null, false, 'email')\n ->willThrowException($exception);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('info')\n ->with(\n '[Hubspot] Failed to fetch contact',\n [\n 'email' => $email,\n 'reason' => 'Contact not found',\n ]\n );\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger($loggerMock);\n\n $result = $client->getContactByEmail($email, $fields);\n\n $this->assertEquals([], $result);\n }\n\n public function testGetOpportunityById(): void\n {\n $opportunityId = '12345';\n $expectedProperties = [\n 'dealname' => 'Test Opportunity',\n 'amount' => '1000.00',\n 'closedate' => '2024-12-31T23:59:59.999Z',\n 'dealstage' => 'presentationscheduled',\n 'pipeline' => 'default',\n ];\n\n $mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);\n $mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);\n $mockHubspotOpportunity->method('getId')->willReturn($opportunityId);\n $now = new \\DateTimeImmutable();\n $mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);\n $mockHubspotOpportunity->method('getArchived')->willReturn(false);\n\n $this->dealsBasicApiMock\n ->expects($this->once())\n ->method('getById')\n ->willReturn($mockHubspotOpportunity);\n\n // Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.\n // The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]\n // Adjust assertions below based on the actual return structure of your method.\n $result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertEquals($opportunityId, $result['id']);\n }\n\n public function testGetContactById(): void\n {\n $crmId = 'contact-123';\n $fields = ['firstname', 'lastname'];\n $expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];\n\n $mockContact = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations::class);\n $mockContact->method('getId')->willReturn($crmId);\n $mockContact->method('getProperties')->willReturn((object) $expectedProperties);\n\n $contactsApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Contacts\\Api\\BasicApi::class);\n $contactsApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'firstname,lastname')\n ->willReturn($mockContact);\n\n $contactsDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Contacts\\Discovery::class);\n $contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {\n if ($name === 'contacts') {\n return $contactsDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getContactById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testGetAccountById(): void\n {\n $crmId = 'account-123';\n $fields = ['name', 'industry'];\n $expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];\n\n $mockCompany = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations::class);\n $mockCompany->method('getId')->willReturn($crmId);\n $mockCompany->method('getProperties')->willReturn((object) $expectedProperties);\n\n $companiesApiMock = $this->createMock(\\HubSpot\\Client\\Crm\\Companies\\Api\\BasicApi::class);\n $companiesApiMock->expects($this->once())\n ->method('getById')\n ->with($crmId, 'name,industry')\n ->willReturn($mockCompany);\n\n $companiesDiscoveryMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Companies\\Discovery::class);\n $companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);\n\n $crmMock = $this->createMock(\\HubSpot\\Discovery\\Crm\\Discovery::class);\n $crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {\n if ($name === 'companies') {\n return $companiesDiscoveryMock;\n }\n\n return $this->createMock(\\HubSpot\\Discovery\\Crm\\Properties\\Discovery::class);\n });\n\n $discoveryMock = $this->createMock(\\HubSpot\\Discovery\\Discovery::class);\n $discoveryMock->method('__call')->with('crm')->willReturn($crmMock);\n\n $client = $this->createPartialMock(Client::class, ['getNewInstance']);\n $client->method('getNewInstance')->willReturn($discoveryMock);\n $client->setLogger(new \\Psr\\Log\\NullLogger());\n\n $result = $client->getAccountById($crmId, $fields);\n\n $this->assertIsArray($result);\n $this->assertArrayHasKey('id', $result);\n $this->assertArrayHasKey('properties', $result);\n $this->assertEquals($crmId, $result['id']);\n $this->assertEquals((object) $expectedProperties, $result['properties']);\n }\n\n public function testEnsureValidTokenWithNoTokenUpdate(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n $originalToken = 'original_token';\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $accessTokenProperty->setValue($this->client, $originalToken);\n\n // Mock token manager to return null (no refresh needed)\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn(null);\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was not changed\n $this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));\n }\n\n\n\n\n\n\n public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void\n {\n $payload = ['filters' => []];\n $type = 'contacts';\n $offset = 0;\n $total = 0;\n $lastRecordId = null;\n\n $expectedResults = [\n ['id' => 'id_1', 'properties' => []],\n ['id' => 'id_2', 'properties' => []],\n ];\n\n // Mock the pagination service to return a generator\n $this->paginationServiceMock\n ->expects($this->once())\n ->method('getPaginatedDataGenerator')\n ->with(\n $this->client,\n $payload,\n $type,\n $offset,\n $this->anything(),\n $this->anything()\n )\n ->willReturnCallback(function () use ($expectedResults) {\n foreach ($expectedResults as $result) {\n yield $result;\n }\n });\n\n // Execute the pagination\n $results = [];\n foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {\n $results[] = $result;\n }\n\n $this->assertCount(2, $results);\n $this->assertEquals('id_1', $results[0]['id']);\n $this->assertEquals('id_2', $results[1]['id']);\n }\n\n public function testEnsureValidTokenDelegatesToTokenManager(): void\n {\n $socialAccountMock = $this->createMock(SocialAccount::class);\n\n // Set up OAuth account\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, $socialAccountMock);\n\n // Mock token manager to return new token\n $this->tokenManagerMock\n ->expects($this->once())\n ->method('ensureValidToken')\n ->with($socialAccountMock)\n ->willReturn('new_access_token');\n\n // Call ensureValidToken\n $this->client->ensureValidToken();\n\n // Verify access token was updated\n $accessTokenProperty = $reflection->getProperty('accessToken');\n $accessTokenProperty->setAccessible(true);\n $this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));\n }\n\n public function testGetOwnersArchivedWithValidResponse(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'owner1@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => true,\n ],\n ],\n ];\n\n // Create a mock response object\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n // Set up the client to return our test data\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=true'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(true);\n\n // Assert the results\n $this->assertCount(1, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('owner1@example.com', $result[0]->getEmail());\n $this->assertEquals('John Doe', $result[0]->getFullName());\n $this->assertTrue($result[0]->isArchived());\n }\n\n public function testGetOwnersArchivedWithEmptyResponse(): void\n {\n // Create a mock response object with empty results\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn(['results' => []]);\n\n // Set up the client to return empty results\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n // Call the method\n $result = $this->client->getOwnersArchived(false);\n\n // Assert the results\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithInvalidResponse(): void\n {\n // Create a mock response that will throw an exception when toArray is called\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willThrowException(new \\InvalidArgumentException('Invalid JSON'));\n\n // Set up the client to return the problematic response\n $this->client->method('makeRequest')\n ->willReturn($response);\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(true);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithHttpError(): void\n {\n // Set up the client to throw an exception\n $this->client->method('makeRequest')\n ->willThrowException(new \\Exception('HTTP Error'));\n\n // Mock the logger to expect an error message\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with($this->stringContains('Failed to fetch owners'));\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n // Call the method and expect an empty array on error\n $result = $this->client->getOwnersArchived(false);\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetOwnersArchivedWithOwnerCreationException(): void\n {\n $responseData = [\n 'results' => [\n [\n 'id' => '123',\n 'email' => 'valid@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'John',\n 'lastName' => 'Doe',\n 'userId' => 456,\n 'userIdIncludingInactive' => 789,\n 'createdAt' => '2023-01-01T12:00:00Z',\n 'updatedAt' => '2023-01-02T12:00:00Z',\n 'archived' => false,\n ],\n [\n 'id' => '456',\n 'email' => 'invalid@example.com',\n 'type' => 'PERSON',\n 'createdAt' => 'invalid-date-format',\n ],\n [\n 'id' => '789',\n 'email' => 'another@example.com',\n 'type' => 'PERSON',\n 'firstName' => 'Jane',\n 'lastName' => 'Smith',\n 'userId' => 999,\n 'userIdIncludingInactive' => 888,\n 'createdAt' => '2023-01-03T12:00:00Z',\n 'updatedAt' => '2023-01-04T12:00:00Z',\n 'archived' => false,\n ],\n ],\n ];\n\n $response = $this->createMock(\\SevenShores\\Hubspot\\Http\\Response::class);\n $response->method('toArray')\n ->willReturn($responseData);\n\n $this->client->method('makeRequest')\n ->with(\n '/crm/v3/owners',\n 'GET',\n [],\n 'archived=false'\n )\n ->willReturn($response);\n\n $loggerMock = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $loggerMock->expects($this->once())\n ->method('error')\n ->with(\n '[HubSpot] Failed to process owner data',\n $this->callback(function ($context) {\n return isset($context['result']) &&\n isset($context['error']) &&\n $context['result']['id'] === '456' &&\n $context['result']['email'] === 'invalid@example.com' &&\n str_contains($context['error'], 'invalid-date-format');\n })\n );\n\n $reflection = new \\ReflectionClass($this->client);\n $loggerProperty = $reflection->getProperty('log');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($this->client, $loggerMock);\n\n $result = $this->client->getOwnersArchived(false);\n\n $this->assertIsArray($result);\n $this->assertCount(2, $result);\n $this->assertEquals('123', $result[0]->getId());\n $this->assertEquals('valid@example.com', $result[0]->getEmail());\n $this->assertEquals('789', $result[1]->getId());\n $this->assertEquals('another@example.com', $result[1]->getEmail());\n }\n\n public function testMakeRequestWithGetMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n null,\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithGetMethodAndQueryString(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'GET',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n [],\n 'limit=10&offset=0',\n true\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'GET', [], 'limit=10&offset=0');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPostMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'John',\n 'lastname' => 'Doe',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPutMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'firstname' => 'Jane',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'updated' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PUT',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PUT', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithPatchMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $payload = [\n 'properties' => [\n 'email' => 'newemail@example.com',\n ],\n ];\n\n $psrResponse = new Response(200, [], json_encode(['id' => '12345', 'patched' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'PATCH',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => $payload]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'PATCH', $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithDeleteMethod(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['deleted' => true]));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'DELETE',\n 'https://api.hubapi.com/crm/v3/objects/contacts/12345',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts/12345', 'DELETE');\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestWithEmptyPayload(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'ok']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n 'POST',\n 'https://api.hubapi.com/crm/v3/objects/contacts',\n ['json' => []]\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $result = $client->makeRequest('/crm/v3/objects/contacts', 'POST', []);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testMakeRequestPrependsBaseUrl(): void\n {\n $socialAccountService = $this->createMock(SocialAccountService::class);\n $paginationService = $this->createMock(HubspotPaginationService::class);\n $tokenManager = $this->createMock(HubspotTokenManager::class);\n\n $psrResponse = new Response(200, [], json_encode(['status' => 'success']));\n $expectedResponse = new HubspotResponse($psrResponse);\n\n $hubspotClientMock = $this->createMock(HubspotClient::class);\n $hubspotClientMock->expects($this->once())\n ->method('request')\n ->with(\n $this->anything(),\n 'https://api.hubapi.com/test/endpoint',\n $this->anything()\n )\n ->willReturn($expectedResponse);\n\n $factoryMock = $this->createMock(Factory::class);\n $factoryMock->method('getClient')->willReturn($hubspotClientMock);\n\n $client = $this->getMockBuilder(Client::class)\n ->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])\n ->onlyMethods(['getInstance'])\n ->getMock();\n\n $client->method('getInstance')->willReturn($factoryMock);\n $client->setAccessToken(new AccessToken(['access_token' => 'test_token']));\n\n $client->makeRequest('/test/endpoint', 'GET');\n }\n\n public function testGetOpportunitiesByIds(): void\n {\n $crmIds = ['deal1', 'deal2', 'deal3'];\n $fields = ['dealname', 'amount'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getOpportunitiesByIds'));\n }\n\n public function testGetCompaniesByIds(): void\n {\n $crmIds = ['company1', 'company2'];\n $fields = ['name', 'industry'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getCompaniesByIds'));\n }\n\n public function testGetContactsByIds(): void\n {\n $crmIds = ['contact1', 'contact2'];\n $fields = ['firstname', 'lastname'];\n\n // Test basic functionality - method exists and returns array\n $this->assertTrue(method_exists($this->client, 'getContactsByIds'));\n }\n\n public function testBatchReadObjectsEmptyIds(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $result = $method->invokeArgs($this->client, ['deals', [], ['dealname']]);\n\n $this->assertEquals([], $result);\n }\n\n public function testBatchReadObjectsExceedsBatchSize(): void\n {\n $crmIds = array_fill(0, 101, 'deal_id'); // 101 IDs, exceeds limit of 100\n\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\InvalidArgumentException::class);\n $this->expectExceptionMessage('Batch size cannot exceed 100 deals');\n\n $method->invokeArgs($this->client, ['deals', $crmIds, ['dealname']]);\n }\n\n public function testBatchReadObjectsUnsupportedObjectType(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $method = $reflection->getMethod('batchReadObjects');\n $method->setAccessible(true);\n\n $this->expectException(\\Jiminny\\Exceptions\\CrmException::class);\n $this->expectExceptionMessage('Failed to batch fetch unsupported');\n\n $method->invokeArgs($this->client, ['unsupported', ['id1'], ['field1']]);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequest401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Unauthorized', 401);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithBadRequestNon401(): void\n {\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\BadRequest('Bad Request', 400);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleRequestException(): void\n {\n $response = new Response(401, [], 'Unauthorized');\n $exception = new \\GuzzleHttp\\Exception\\RequestException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), $response);\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithGuzzleClientException(): void\n {\n $exception = new \\GuzzleHttp\\Exception\\ClientException('Unauthorized', new \\GuzzleHttp\\Psr7\\Request('GET', 'test'), new Response(401));\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithStringMatching(): void\n {\n $exception = new \\Exception('HTTP 401 Unauthorized error occurred');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertTrue($result);\n }\n\n public function testIsUnauthorizedExceptionWithNonUnauthorized(): void\n {\n $exception = new \\Exception('Some other error');\n\n $result = $this->client->isUnauthorizedException($exception);\n\n $this->assertFalse($result);\n }\n\n public function testEnsureValidTokenWithNullOauthAccount(): void\n {\n $reflection = new \\ReflectionClass($this->client);\n $oauthAccountProperty = $reflection->getProperty('oauthAccount');\n $oauthAccountProperty->setAccessible(true);\n $oauthAccountProperty->setValue($this->client, null);\n\n // Should not throw exception and not call token manager\n $this->tokenManagerMock->expects($this->never())->method('ensureValidToken');\n\n $this->client->ensureValidToken();\n\n $this->assertTrue(true); // Test passes if no exception is thrown\n }\n\n public function testGetConfig(): void\n {\n $result = $this->client->getConfig();\n\n $this->assertSame($this->config, $result);\n }\n\n public function testGetOwners(): void\n {\n // Test that the method exists and can be called\n $this->assertTrue(method_exists($this->client, 'getOwners'));\n\n // Since getOwners has complex HubSpot API dependencies,\n // we'll just verify the method exists for now\n $this->assertIsCallable([$this->client, 'getOwners']);\n }\n\n public function testCreateMeeting(): void\n {\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Test Meeting',\n 'hs_meeting_start_time' => '2024-01-01T10:00:00Z',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => 'meeting123']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with('/crm/v3/objects/meetings', 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->createMeeting($payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testUpdateMeeting(): void\n {\n $meetingId = 'meeting123';\n $payload = [\n 'properties' => [\n 'hs_meeting_title' => 'Updated Meeting',\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['id' => $meetingId, 'updated' => true]);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v3/objects/meetings/{$meetingId}\", 'PATCH', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->updateMeeting($meetingId, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testAddAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/create\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->addAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n public function testRemoveAssociations(): void\n {\n $objectType = 'deals';\n $associationType = 'contacts';\n $payload = [\n 'inputs' => [\n ['from' => ['id' => 'deal1'], 'to' => ['id' => 'contact1']],\n ],\n ];\n\n $expectedResponse = $this->generateHubSpotResponse(['status' => 'success']);\n\n $this->client->expects($this->once())\n ->method('makeRequest')\n ->with(\"/crm/v4/associations/{$objectType}/{$associationType}/batch/archive\", 'POST', $payload)\n ->willReturn($expectedResponse);\n\n $result = $this->client->removeAssociations($objectType, $associationType, $payload);\n\n $this->assertSame($expectedResponse, $result);\n }\n\n // -------------------------------------------------------------------------\n // search() / executeRequest() tests\n // -------------------------------------------------------------------------\n\n public function testSearchReturnsDecodedArrayOnSuccess(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $payload = ['filters' => []];\n $responseData = ['results' => [['id' => '1']], 'total' => 1, 'paging' => []];\n $hubspotResponse = new HubspotResponse(new Response(200, [], json_encode($responseData)));\n\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->with('POST', Client::BASE_URL . '/crm/v3/objects/contacts/search', ['json' => $payload])\n ->willReturn($hubspotResponse);\n\n $result = $this->client->search('contacts', $payload);\n\n $this->assertSame($responseData, $result);\n\n Mockery::close();\n }\n\n public function testSearchThrowsRateLimitExceptionWhenCircuitBreakerActive(): void\n {\n $futureTimestamp = (string) (time() + 30);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(42);\n\n $this->hubspotClientMock->expects($this->never())->method('request');\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot rate limit (cached circuit-breaker)');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchThrowsRateLimitExceptionAndSetsNxOnFresh429(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n // NX + TTL option array — exact TTL depends on parseRetryAfter, verified separately\n $redisMock->shouldReceive('set')\n ->once()\n ->with(\n 'hubspot:ratelimit:config:42',\n Mockery::type('string'),\n Mockery::on(fn ($opts) => is_array($opts) && in_array('nx', $opts, true))\n );\n\n $this->config->method('getId')->willReturn(42);\n\n $badRequest = new BadRequest('Rate limit exceeded', 429);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($badRequest);\n\n $this->expectException(RateLimitException::class);\n $this->expectExceptionMessage('Hubspot returned 429');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchPropagatesNonRateLimitException(): void\n {\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn(null);\n\n $this->config->method('getId')->willReturn(42);\n\n $exception = new \\SevenShores\\Hubspot\\Exceptions\\HubspotException('Server error', 500);\n $this->hubspotClientMock\n ->expects($this->once())\n ->method('request')\n ->willThrowException($exception);\n\n $this->expectException(\\SevenShores\\Hubspot\\Exceptions\\HubspotException::class);\n $this->expectExceptionMessage('Server error');\n\n try {\n $this->client->search('contacts', []);\n } finally {\n Mockery::close();\n }\n }\n\n public function testSearchCircuitBreakerRetryAfterComputedFromStoredTimestamp(): void\n {\n $remaining = 45;\n $futureTimestamp = (string) (time() + $remaining);\n\n $redisMock = Mockery::mock('alias:Illuminate\\Support\\Facades\\Redis');\n $redisMock->shouldReceive('get')->once()->andReturn($futureTimestamp);\n\n $this->config->method('getId')->willReturn(1);\n\n try {\n $this->client->search('deals', []);\n $this->fail('Expected RateLimitException');\n } catch (RateLimitException $e) {\n $this->assertGreaterThanOrEqual(1, $e->getRetryAfter());\n $this->assertLessThanOrEqual($remaining, $e->getRetryAfter());\n } finally {\n Mockery::close();\n }\n }\n\n // -------------------------------------------------------------------------\n // isHubspotRateLimit() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callIsHubspotRateLimit(\\Throwable $e): bool\n {\n $method = new \\ReflectionMethod($this->client, 'isHubspotRateLimit');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testIsHubspotRateLimitReturnsTrueForBadRequest429(): void\n {\n $e = new BadRequest('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForDealApiException429(): void\n {\n $e = new DealApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForContactApiException429(): void\n {\n $e = new ContactApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForCompanyApiException429(): void\n {\n $e = new CompanyApiException('Too Many Requests', 429);\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsTrueForGuzzleRequestException429(): void\n {\n $request = new \\GuzzleHttp\\Psr7\\Request('POST', 'https://api.hubapi.com/test');\n $response = new \\GuzzleHttp\\Psr7\\Response(429);\n $e = new \\GuzzleHttp\\Exception\\RequestException('Too Many Requests', $request, $response);\n\n $this->assertTrue($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForBadRequestNon429(): void\n {\n $e = new BadRequest('Bad Request', 400);\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n public function testIsHubspotRateLimitReturnsFalseForGenericException(): void\n {\n $e = new \\RuntimeException('Something went wrong');\n $this->assertFalse($this->callIsHubspotRateLimit($e));\n }\n\n // -------------------------------------------------------------------------\n // parseRetryAfter() tests (private method — tested via reflection)\n // -------------------------------------------------------------------------\n\n private function callParseRetryAfter(\\Throwable $e): int\n {\n $method = new \\ReflectionMethod($this->client, 'parseRetryAfter');\n $method->setAccessible(true);\n\n return $method->invoke($this->client, $e);\n }\n\n public function testParseRetryAfterReadsRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['30']]);\n $this->assertSame(30, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReadsLowercaseRetryAfterHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['retry-after' => ['15']]);\n $this->assertSame(15, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterHandlesScalarHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => '60']);\n $this->assertSame(60, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDailyLimitDelay(): void\n {\n $e = new BadRequest('Daily rate limit exceeded');\n $this->assertSame(600, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsTenSecondlyDelay(): void\n {\n $e = new BadRequest('Ten secondly rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsSecondlyDelay(): void\n {\n $e = new BadRequest('Secondly rate limit exceeded');\n $this->assertSame(1, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterReturnsDefaultWhenNoHint(): void\n {\n $e = new BadRequest('Rate limit exceeded');\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n\n public function testParseRetryAfterIgnoresNonNumericHeader(): void\n {\n $e = new DealApiException('Too Many Requests', 429, ['Retry-After' => ['not-a-number']]);\n // Falls through to message parsing; \"Too Many Requests\" has no known keyword → default 10\n $this->assertSame(10, $this->callParseRetryAfter($e));\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","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":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
4682894321548166980
|
4446428687123393012
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
19
144
11
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Hubspot;
use GuzzleHttp\Psr7\Response;
use HubSpot\Client\Crm\Associations\Api\BatchApi;
use HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId;
use HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti;
use HubSpot\Client\Crm\Associations\Model\PublicObjectId;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Deals\Api\BasicApi as DealsBasicApi;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Pipelines\Api\PipelinesApi;
use HubSpot\Client\Crm\Pipelines\Model\CollectionResponsePipeline;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\Pipeline;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Api\CoreApi;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery as HubSpotDiscovery;
use HubSpot\Discovery\Crm\Deals\Discovery as DealsDiscovery;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\SocialAccount;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\HubspotTokenManager;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Jiminny\Services\SocialAccountService;
use League\OAuth2\Client\Token\AccessToken;
use Mockery;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use SevenShores\Hubspot\Endpoints\Engagements;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Client as HubspotClient;
use SevenShores\Hubspot\Http\Response as HubspotResponse;
/**
* @runTestsInSeparateProcesses
*
* @preserveGlobalState disabled
*/
class ClientTest extends TestCase
{
private const string RESPONSE_TYPE_STAGE_FIELD = 'stage';
private const string RESPONSE_TYPE_PIPELINE_FIELD = 'pipeline';
private const string RESPONSE_TYPE_REGULAR_FIELD = 'regular';
/**
* @var Client&MockObject
*/
private Client $client;
private Configuration $config;
/**
* @var SocialAccountService&MockObject
*/
private SocialAccountService $socialAccountServiceMock;
/**
* @var HubspotPaginationService&MockObject
*/
private HubspotPaginationService $paginationServiceMock;
/**
* @var HubspotTokenManager&MockObject
*/
private HubspotTokenManager $tokenManagerMock;
/**
* @var CoreApi&MockObject
*/
private CoreApi $coreApiMock;
/**
* @var PipelinesApi&MockObject
*/
private PipelinesApi $pipelinesApiMock;
/**
* @var BatchApi&MockObject
*/
private BatchApi $associationsBatchApiMock;
/**
* @var DealsBasicApi&MockObject
*/
private DealsBasicApi $dealsBasicApiMock;
/**
* @var Engagements&MockObject
*/
private $engagementsMock;
/**
* @var HubspotClient&MockObject
*/
private HubspotClient $hubspotClientMock;
protected function setUp(): void
{
// Create mocks for dependencies
$this->socialAccountServiceMock = $this->createMock(SocialAccountService::class);
$this->paginationServiceMock = $this->createMock(HubspotPaginationService::class);
$this->tokenManagerMock = $this->createMock(HubspotTokenManager::class);
// Create a real Client instance with mocked dependencies
// Create a partial mock only for the methods we need to mock
$client = $this->createPartialMock(Client::class, ['getInstance', 'getNewInstance', 'makeRequest']);
$client->setLogger(new NullLogger());
// Inject the real dependencies using reflection
$reflection = new \ReflectionClass(Client::class);
$accountServiceProperty = $reflection->getProperty('accountService');
$accountServiceProperty->setAccessible(true);
$accountServiceProperty->setValue($client, $this->socialAccountServiceMock);
$paginationServiceProperty = $reflection->getProperty('paginationService');
$paginationServiceProperty->setAccessible(true);
$paginationServiceProperty->setValue($client, $this->paginationServiceMock);
$tokenManagerProperty = $reflection->getProperty('tokenManager');
$tokenManagerProperty->setAccessible(true);
$tokenManagerProperty->setValue($client, $this->tokenManagerMock);
$factoryMock = $this->createMock(Factory::class);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$engagementsMock = $this->createMock(\SevenShores\Hubspot\Endpoints\Engagements::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$factoryMock->method('__call')->with('engagements')->willReturn($engagementsMock);
$client->method('getInstance')->willReturn($factoryMock);
$discoveryMock = $this->createMock(HubSpotDiscovery\Discovery::class);
$crmMock = $this->createMock(HubSpotDiscovery\Crm\Discovery::class);
$associationsMock = $this->createMock(HubSpotDiscovery\Crm\Associations\Discovery::class);
$propertiesMock = $this->createMock(HubSpotDiscovery\Crm\Properties\Discovery::class);
$pipelinesMock = $this->createMock(HubSpotDiscovery\Crm\Pipelines\Discovery::class);
$coreApiMock = $this->createMock(CoreApi::class);
$pipelinesApiMock = $this->createMock(PipelinesApi::class);
$associationsBatchApiMock = $this->createMock(BatchApi::class);
$dealsBasicApiMock = $this->createMock(DealsBasicApi::class);
$propertiesMock->method('__call')->with('coreApi')->willReturn($coreApiMock);
$pipelinesMock->method('__call')->with('pipelinesApi')->willReturn($pipelinesApiMock);
$associationsMock->method('__call')->with('batchApi')->willReturn($associationsBatchApiMock);
$dealsDiscoveryMock = $this->createMock(DealsDiscovery::class);
$dealsDiscoveryMock->method('__call')->with('basicApi')->willReturn($dealsBasicApiMock);
$returnMap = ['properties' => $propertiesMock, 'pipelines' => $pipelinesMock, 'associations' => $associationsMock, 'deals' => $dealsDiscoveryMock];
$crmMock->method('__call')
->willReturnCallback(static fn (string $name) => $returnMap[$name])
;
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client->method('getNewInstance')->willReturn($discoveryMock);
$this->client = $client;
$this->config = $this->createMock(Configuration::class);
$this->client->setConfiguration($this->config);
$this->coreApiMock = $coreApiMock;
$this->pipelinesApiMock = $pipelinesApiMock;
$this->associationsBatchApiMock = $associationsBatchApiMock;
$this->dealsBasicApiMock = $dealsBasicApiMock;
$this->engagementsMock = $engagementsMock;
$this->hubspotClientMock = $hubspotClientMock;
}
public function testGetMinimumApiVersion(): void
{
$this->assertIsString($this->client->getMinimumApiVersion());
}
public function testGetInstance(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$client = new Client($socialAccountService, $paginationService, $tokenManager);
$client->setBaseUrl('[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]))
;
$this->assertEquals(
[
['id' => 'foo', 'label' => 'bar'],
['id' => 'baz', 'label' => 'qux'],
],
$this->client->fetchOpportunityPipelineStages()
);
}
public function testFetchOpportunityPipelineStagesErrorResponse(): void
{
$this->pipelinesApiMock
->method('getAll')
->with('deals')
->willReturn(new Error(['message' => 'test error']))
;
$this->assertEmpty($this->client->fetchOpportunityPipelineStages());
}
#[\PHPUnit\Framework\Attributes\DataProvider('meetingOutcomeFieldProvider')]
public function testFetchMeetingOutcomeFieldOptions(string $fieldId, string $expectedEndpoint): void
{
$field = new Field(['crm_provider_id' => $fieldId]);
$this->hubspotClientMock
->expects($this->once())
->method('request')
->with('GET', $expectedEndpoint)
->willReturn($this->generateHubSpotResponse(
[
'options' => [
['value' => 'option_1', 'label' => 'Option 1', 'displayOrder' => 0],
['value' => 'option_2', 'label' => 'Option 2', 'displayOrder' => 1],
['value' => 'option_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]
))
;
$this->assertEquals(
[
['id' => 'option_1', 'value' => 'option_1', 'label' => 'Option 1', 'display_order' => 0],
['id' => 'option_2', 'value' => 'option_2', 'label' => 'Option 2', 'display_order' => 1],
['id' => 'option_3', 'value' => 'option_3', 'label' => 'Option 3', 'display_order' => 2],
],
$this->client->fetchMeetingOutcomeFieldOptions($field)
);
}
public static function meetingOutcomeFieldProvider(): array
{
return [
'meeting outcome field' => [
'meetingOutcome',
'[URL_WITH_CREDENTIALS] The class CollectionResponsePipeline will be deprecated in the next Hubspot version
*/
$pipelineStagesResponse = new CollectionResponsePipeline([
'results' => [$this->generatePipeline()],
]);
$this->pipelinesApiMock
->method('getAll')
->willReturn($pipelineStagesResponse);
}
if ($type === self::RESPONSE_TYPE_PIPELINE_FIELD) {
$field->method('isStageField')->willReturn(false);
$field->method('isPipelineField')->willReturn(true);
$pipelineResponse = $this->generateHubSpotResponse([
'results' => [
['id' => '123', 'label' => 'Sales'],
['id' => 'default', 'label' => 'CS'],
],
]);
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($pipelineResponse);
}
$this->assertEquals(
$responses[$type],
$this->client->fetchOpportunityFieldOptions($field)
);
}
public static function opportunityFieldOptionsProvider(): array
{
return [
'stage field' => [
'field' => new Field(['crm_provider_id' => 'dealstage']),
'type' => self::RESPONSE_TYPE_STAGE_FIELD,
],
'pipeline field' => [
'field' => new Field(['crm_provider_id' => 'pipeline']),
'type' => self::RESPONSE_TYPE_PIPELINE_FIELD,
],
'regular field' => [
'field' => new Field(['crm_provider_id' => 'some_property']),
'type' => self::RESPONSE_TYPE_REGULAR_FIELD,
],
];
}
private function generateHubSpotResponse(array $data): HubspotResponse
{
return new HubspotResponse(new Response(200, [], json_encode($data)));
}
private function generateProperty(): Property
{
return new Property([
'name' => 'some_property',
'options' => [
[
'label' => 'label_1',
'value' => 'value_1',
],
[
'label' => 'label_2',
'value' => 'value_2',
],
],
]);
}
private function generatePipeline(): Pipeline
{
return new Pipeline(['stages' => [
new PipelineStage(['id' => 'foo', 'label' => 'bar']),
new PipelineStage(['id' => 'baz', 'label' => 'qux']),
]]);
}
public function testFetchOpportunityPipelines(): void
{
$this->client
->method('makeRequest')
->with('/crm/v3/pipelines/deals')
->willReturn($this->generateHubSpotResponse([
'results' => [
['id' => 'id_1', 'label' => 'Option 1', 'displayOrder' => 0],
['id' => 'id_2', 'label' => 'Option 2', 'displayOrder' => 1],
['id' => 'id_3', 'label' => 'Option 3', 'displayOrder' => 2],
],
]));
$this->assertEquals(
[
['id' => 'id_1', 'label' => 'Option 1'],
['id' => 'id_2', 'label' => 'Option 2'],
['id' => 'id_3', 'label' => 'Option 3'],
],
$this->client->fetchOpportunityPipelines()
);
}
public function testGetPaginatedData(): void
{
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
['id' => 'id_3', 'properties' => []],
];
// Mock the pagination service to return a generator and modify reference parameters
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
['payload_key' => 'payload_value'],
'foobar',
0,
$this->anything(),
$this->anything()
)
->willReturnCallback(function ($client, $payload, $type, $offset, &$total, &$lastRecordId) use ($expectedResults) {
$total = 3;
$lastRecordId = 'id_3';
foreach ($expectedResults as $result) {
yield $result;
}
});
$this->assertEquals(
[
'results' => $expectedResults,
'total' => 3,
'last_record' => 'id_3',
],
$this->client->getPaginatedData(['payload_key' => 'payload_value'], 'foobar')
);
}
public function testGetAssociationsData(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$responseResults = [];
foreach ($ids as $id) {
$from = new PublicObjectId();
$from->setId($id);
$to1 = new PublicObjectId();
$to1->setId('contact_' . $id . '_1');
$to2 = new PublicObjectId();
$to2->setId('contact_' . $id . '_2');
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to1, $to2]);
$responseResults[] = $result;
}
$batchResponse = new BatchResponsePublicAssociationMulti();
$batchResponse->setResults($responseResults);
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function (BatchInputPublicObjectId $batchInput) use ($ids) {
$inputIds = array_map(
fn ($input) => $input->getId(),
$batchInput->getInputs()
);
return $inputIds === $ids;
})
)
->willReturn($batchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$expectedResult = [
'1' => ['contact_1_1', 'contact_1_2'],
'2' => ['contact_2_1', 'contact_2_2'],
'3' => ['contact_3_1', 'contact_3_2'],
];
$this->assertEquals($expectedResult, $result);
}
public function testGetAssociationsDataHandlesException(): void
{
$ids = ['1', '2', '3'];
$fromObject = 'deals';
$toObject = 'contacts';
$exception = new \Exception('API Error');
$this->associationsBatchApiMock->expects($this->once())
->method('read')
->with(
$this->equalTo($fromObject),
$this->equalTo($toObject),
$this->callback(function ($batchInput) use ($ids) {
return $batchInput instanceof \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId
&& count($batchInput->getInputs()) === count($ids);
})
)
->willThrowException($exception);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[Hubspot] Failed to fetch associations',
[
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => 'API Error',
]
);
$this->client->setLogger($loggerMock);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertEmpty($result);
$this->assertIsArray($result);
}
public function testGetAssociationsDataWithLargeDataSet(): void
{
$ids = array_map(fn ($i) => (string) $i, range(1, 2500)); // More than 1000 items
$fromObject = 'deals';
$toObject = 'contacts';
$firstBatchResponse = new BatchResponsePublicAssociationMulti();
$firstBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, 0, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$firstBatchResponse->setResults($firstBatchResults);
$secondBatchResponse = new BatchResponsePublicAssociationMulti();
$secondBatchResults = array_map(function ($id) {
$from = new PublicObjectId();
$from->setId($id);
$to = new PublicObjectId();
$to->setId('contact_' . $id);
$result = new \HubSpot\Client\Crm\Associations\Model\PublicAssociationMulti();
$result->setFrom($from);
$result->setTo([$to]);
return $result;
}, array_slice($ids, Client::ASSOCIATIONS_BATCH_SIZE_LIMIT));
$secondBatchResponse->setResults($secondBatchResults);
$this->associationsBatchApiMock->expects($this->exactly(3))
->method('read')
->willReturnOnConsecutiveCalls($firstBatchResponse, $secondBatchResponse);
$result = $this->client->getAssociationsData($ids, $fromObject, $toObject);
$this->assertCount(2500, $result);
$this->assertArrayHasKey('1', $result);
$this->assertArrayHasKey('2500', $result);
$this->assertEquals(['contact_1'], $result['1']);
$this->assertEquals(['contact_2500'], $result['2500']);
}
public function testGetContactByEmailSuccess(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname', 'email'];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn([
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
]);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname,email', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => [
'firstname' => 'John',
'lastname' => 'Doe',
'email' => '[EMAIL]',
],
], $result);
}
public function testGetContactByEmailWithEmptyFields(): void
{
$email = '[EMAIL]';
$fields = [];
$contactMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$contactMock->method('getId')->willReturn('12345');
$contactMock->method('getProperties')->willReturn(['email' => '[EMAIL]']);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, '', null, false, 'email')
->willReturn($contactMock);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new NullLogger());
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([
'id' => '12345',
'properties' => ['email' => '[EMAIL]'],
], $result);
}
public function testGetContactByEmailApiException(): void
{
$email = '[EMAIL]';
$fields = ['firstname', 'lastname'];
$exception = new \HubSpot\Client\Crm\Contacts\ApiException('Contact not found', 404);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($email, 'firstname,lastname', null, false, 'email')
->willThrowException($exception);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with(
'[Hubspot] Failed to fetch contact',
[
'email' => $email,
'reason' => 'Contact not found',
]
);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger($loggerMock);
$result = $client->getContactByEmail($email, $fields);
$this->assertEquals([], $result);
}
public function testGetOpportunityById(): void
{
$opportunityId = '12345';
$expectedProperties = [
'dealname' => 'Test Opportunity',
'amount' => '1000.00',
'closedate' => '2024-12-31T23:59:59.999Z',
'dealstage' => 'presentationscheduled',
'pipeline' => 'default',
];
$mockHubspotOpportunity = $this->createMock(DealWithAssociations::class);
$mockHubspotOpportunity->method('getProperties')->willReturn((object) $expectedProperties);
$mockHubspotOpportunity->method('getId')->willReturn($opportunityId);
$now = new \DateTimeImmutable();
$mockHubspotOpportunity->method('getCreatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getUpdatedAt')->willReturn($now);
$mockHubspotOpportunity->method('getArchived')->willReturn(false);
$this->dealsBasicApiMock
->expects($this->once())
->method('getById')
->willReturn($mockHubspotOpportunity);
// Assuming Client::getOpportunityById processes the SimplePublicObject and returns an associative array.
// The structure might be like: ['id' => ..., 'properties' => [...], 'createdAt' => ..., ...]
// Adjust assertions below based on the actual return structure of your method.
$result = $this->client->getOpportunityById($opportunityId, ['test', 'test']);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertEquals($opportunityId, $result['id']);
}
public function testGetContactById(): void
{
$crmId = 'contact-123';
$fields = ['firstname', 'lastname'];
$expectedProperties = ['firstname' => 'John', 'lastname' => 'Doe'];
$mockContact = $this->createMock(\HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations::class);
$mockContact->method('getId')->willReturn($crmId);
$mockContact->method('getProperties')->willReturn((object) $expectedProperties);
$contactsApiMock = $this->createMock(\HubSpot\Client\Crm\Contacts\Api\BasicApi::class);
$contactsApiMock->expects($this->once())
->method('getById')
->with($crmId, 'firstname,lastname')
->willReturn($mockContact);
$contactsDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Contacts\Discovery::class);
$contactsDiscoveryMock->method('__call')->with('basicApi')->willReturn($contactsApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($contactsDiscoveryMock) {
if ($name === 'contacts') {
return $contactsDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getContactById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testGetAccountById(): void
{
$crmId = 'account-123';
$fields = ['name', 'industry'];
$expectedProperties = ['name' => 'Acme Corp', 'industry' => 'Technology'];
$mockCompany = $this->createMock(\HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations::class);
$mockCompany->method('getId')->willReturn($crmId);
$mockCompany->method('getProperties')->willReturn((object) $expectedProperties);
$companiesApiMock = $this->createMock(\HubSpot\Client\Crm\Companies\Api\BasicApi::class);
$companiesApiMock->expects($this->once())
->method('getById')
->with($crmId, 'name,industry')
->willReturn($mockCompany);
$companiesDiscoveryMock = $this->createMock(\HubSpot\Discovery\Crm\Companies\Discovery::class);
$companiesDiscoveryMock->method('__call')->with('basicApi')->willReturn($companiesApiMock);
$crmMock = $this->createMock(\HubSpot\Discovery\Crm\Discovery::class);
$crmMock->method('__call')->willReturnCallback(function ($name) use ($companiesDiscoveryMock) {
if ($name === 'companies') {
return $companiesDiscoveryMock;
}
return $this->createMock(\HubSpot\Discovery\Crm\Properties\Discovery::class);
});
$discoveryMock = $this->createMock(\HubSpot\Discovery\Discovery::class);
$discoveryMock->method('__call')->with('crm')->willReturn($crmMock);
$client = $this->createPartialMock(Client::class, ['getNewInstance']);
$client->method('getNewInstance')->willReturn($discoveryMock);
$client->setLogger(new \Psr\Log\NullLogger());
$result = $client->getAccountById($crmId, $fields);
$this->assertIsArray($result);
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('properties', $result);
$this->assertEquals($crmId, $result['id']);
$this->assertEquals((object) $expectedProperties, $result['properties']);
}
public function testEnsureValidTokenWithNoTokenUpdate(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
$originalToken = 'original_token';
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$accessTokenProperty->setValue($this->client, $originalToken);
// Mock token manager to return null (no refresh needed)
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn(null);
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was not changed
$this->assertEquals($originalToken, $accessTokenProperty->getValue($this->client));
}
public function testGetPaginatedDataGeneratorDelegatesToPaginationService(): void
{
$payload = ['filters' => []];
$type = 'contacts';
$offset = 0;
$total = 0;
$lastRecordId = null;
$expectedResults = [
['id' => 'id_1', 'properties' => []],
['id' => 'id_2', 'properties' => []],
];
// Mock the pagination service to return a generator
$this->paginationServiceMock
->expects($this->once())
->method('getPaginatedDataGenerator')
->with(
$this->client,
$payload,
$type,
$offset,
$this->anything(),
$this->anything()
)
->willReturnCallback(function () use ($expectedResults) {
foreach ($expectedResults as $result) {
yield $result;
}
});
// Execute the pagination
$results = [];
foreach ($this->client->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastRecordId) as $result) {
$results[] = $result;
}
$this->assertCount(2, $results);
$this->assertEquals('id_1', $results[0]['id']);
$this->assertEquals('id_2', $results[1]['id']);
}
public function testEnsureValidTokenDelegatesToTokenManager(): void
{
$socialAccountMock = $this->createMock(SocialAccount::class);
// Set up OAuth account
$reflection = new \ReflectionClass($this->client);
$oauthAccountProperty = $reflection->getProperty('oauthAccount');
$oauthAccountProperty->setAccessible(true);
$oauthAccountProperty->setValue($this->client, $socialAccountMock);
// Mock token manager to return new token
$this->tokenManagerMock
->expects($this->once())
->method('ensureValidToken')
->with($socialAccountMock)
->willReturn('new_access_token');
// Call ensureValidToken
$this->client->ensureValidToken();
// Verify access token was updated
$accessTokenProperty = $reflection->getProperty('accessToken');
$accessTokenProperty->setAccessible(true);
$this->assertEquals('new_access_token', $accessTokenProperty->getValue($this->client));
}
public function testGetOwnersArchivedWithValidResponse(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => true,
],
],
];
// Create a mock response object
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
// Set up the client to return our test data
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=true'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(true);
// Assert the results
$this->assertCount(1, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('John Doe', $result[0]->getFullName());
$this->assertTrue($result[0]->isArchived());
}
public function testGetOwnersArchivedWithEmptyResponse(): void
{
// Create a mock response object with empty results
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn(['results' => []]);
// Set up the client to return empty results
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
// Call the method
$result = $this->client->getOwnersArchived(false);
// Assert the results
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithInvalidResponse(): void
{
// Create a mock response that will throw an exception when toArray is called
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willThrowException(new \InvalidArgumentException('Invalid JSON'));
// Set up the client to return the problematic response
$this->client->method('makeRequest')
->willReturn($response);
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(true);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithHttpError(): void
{
// Set up the client to throw an exception
$this->client->method('makeRequest')
->willThrowException(new \Exception('HTTP Error'));
// Mock the logger to expect an error message
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with($this->stringContains('Failed to fetch owners'));
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
// Call the method and expect an empty array on error
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetOwnersArchivedWithOwnerCreationException(): void
{
$responseData = [
'results' => [
[
'id' => '123',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'John',
'lastName' => 'Doe',
'userId' => 456,
'userIdIncludingInactive' => 789,
'createdAt' => '2023-01-01T12:00:00Z',
'updatedAt' => '2023-01-02T12:00:00Z',
'archived' => false,
],
[
'id' => '456',
'email' => '[EMAIL]',
'type' => 'PERSON',
'createdAt' => 'invalid-date-format',
],
[
'id' => '789',
'email' => '[EMAIL]',
'type' => 'PERSON',
'firstName' => 'Jane',
'lastName' => 'Smith',
'userId' => 999,
'userIdIncludingInactive' => 888,
'createdAt' => '2023-01-03T12:00:00Z',
'updatedAt' => '2023-01-04T12:00:00Z',
'archived' => false,
],
],
];
$response = $this->createMock(\SevenShores\Hubspot\Http\Response::class);
$response->method('toArray')
->willReturn($responseData);
$this->client->method('makeRequest')
->with(
'/crm/v3/owners',
'GET',
[],
'archived=false'
)
->willReturn($response);
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
$loggerMock->expects($this->once())
->method('error')
->with(
'[HubSpot] Failed to process owner data',
$this->callback(function ($context) {
return isset($context['result']) &&
isset($context['error']) &&
$context['result']['id'] === '456' &&
$context['result']['email'] === '[EMAIL]' &&
str_contains($context['error'], 'invalid-date-format');
})
);
$reflection = new \ReflectionClass($this->client);
$loggerProperty = $reflection->getProperty('log');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($this->client, $loggerMock);
$result = $this->client->getOwnersArchived(false);
$this->assertIsArray($result);
$this->assertCount(2, $result);
$this->assertEquals('123', $result[0]->getId());
$this->assertEquals('[EMAIL]', $result[0]->getEmail());
$this->assertEquals('789', $result[1]->getId());
$this->assertEquals('[EMAIL]', $result[1]->getEmail());
}
public function testMakeRequestWithGetMethod(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManager = $this->createMock(HubspotTokenManager::class);
$psrResponse = new Response(200, [], json_encode(['status' => 'success']));
$expectedResponse = new HubspotResponse($psrResponse);
$hubspotClientMock = $this->createMock(HubspotClient::class);
$hubspotClientMock->expects($this->once())
->method('request')
->with(
'GET',
'https://api.hubapi.com/crm/v3/objects/contacts',
[],
null,
true
)
->willReturn($expectedResponse);
$factoryMock = $this->createMock(Factory::class);
$factoryMock->method('getClient')->willReturn($hubspotClientMock);
$client = $this->getMockBuilder(Client::class)
->setConstructorArgs([$socialAccountService, $paginationService, $tokenManager])
->onlyMethods(['getInstance'])
->getMock();
$client->method('getInstance')->willReturn($factoryMock);
$client->setAccessToken(new AccessToken(['access_token' => 'test_token']));
$result = $client->makeRequest('/crm/v3/objects/contacts', 'GET');
$this->assertSame($expectedResponse, $result);
}
public function testMakeRequestWithGetMethodAndQueryString(): void
{
$socialAccountService = $this->createMock(SocialAccountService::class);
$paginationService = $this->createMock(HubspotPaginationService::class);
$tokenManag...
|
20508
|
NULL
|
NULL
|
NULL
|
|
20523
|
890
|
3
|
2026-05-11T15:46:18.743391+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514378743_m1.jpg...
|
PhpStorm
|
faVsco.js – ClientTest.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp> 0 hhlDOCKERAPP (-zsh)-zsh++- ₴81DEV (docker)₴2APP (-zsh)ScrmService->syncOpportunity('374720564');ScrmService-›matchByName('Robot');-zsh*5ffmpeg100% <78• Mon 11 May 18:46:18T₴1S- ₴6-zsh*7 |+end diffAPPFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1Learn moreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfixdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diffPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.3.30Running analysis on 7 cores with 10 files per process.Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!Loadedconfig default from".php-cs-fixer.dist.php".5666/5666 [100%Fixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image » docker debug docker_1amp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ I...
|
NULL
|
-6826975386075694953
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp> 0 hhlDOCKERAPP (-zsh)-zsh++- ₴81DEV (docker)₴2APP (-zsh)ScrmService->syncOpportunity('374720564');ScrmService-›matchByName('Robot');-zsh*5ffmpeg100% <78• Mon 11 May 18:46:18T₴1S- ₴6-zsh*7 |+end diffAPPFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1Learn moreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfixdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diffPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.3.30Running analysis on 7 cores with 10 files per process.Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!Loadedconfig default from".php-cs-fixer.dist.php".5666/5666 [100%Fixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image » docker debug docker_1amp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ I...
|
20508
|
NULL
|
NULL
|
NULL
|
|
20525
|
890
|
4
|
2026-05-11T15:46:20.525347+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514380525_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp> 0 hhlDOCKERAPP (-zsh)-zsh++₴81DEV (docker)₴2APP (-zsh)ScrmService->syncOpportunity('374720564');ScrmService-›matchByName('Robot');-zsh*5ffmpeg100% <78• Mon 11 May 18:46:20T₴1O ₴6-zsh*7 |+end diffAPPFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1Learn moreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfixdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diffPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.3.30Running analysis on 7 cores with 10 files per process.Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!Loadedconfig default from".php-cs-fixer.dist.php".5666/5666 [100%Fixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image » docker debug docker_1amp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ I...
|
NULL
|
-3905535018503763461
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp> 0 hhlDOCKERAPP (-zsh)-zsh++₴81DEV (docker)₴2APP (-zsh)ScrmService->syncOpportunity('374720564');ScrmService-›matchByName('Robot');-zsh*5ffmpeg100% <78• Mon 11 May 18:46:20T₴1O ₴6-zsh*7 |+end diffAPPFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1Learn moreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfixdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diffPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.3.30Running analysis on 7 cores with 10 files per process.Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!Loadedconfig default from".php-cs-fixer.dist.php".5666/5666 [100%Fixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image » docker debug docker_1amp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ I...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
20527
|
890
|
5
|
2026-05-11T15:46:52.791867+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514412791_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
67
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), ['nx', 'ex' => $retryAfter]);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $this->executeRequest(fn () => $batchConfig['api']->read($batchReadRequest));
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (RateLimitException $e) {
throw $e;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
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-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"ClientTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'ClientTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'ClientTest'","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":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"67","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), ['nx', 'ex' => $retryAfter]);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n\n $response = $this->executeRequest(fn () => $batchConfig['api']->read($batchReadRequest));\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (RateLimitException $e) {\n throw $e;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), ['nx', 'ex' => $retryAfter]);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n\n $response = $this->executeRequest(fn () => $batchConfig['api']->read($batchReadRequest));\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (RateLimitException $e) {\n throw $e;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"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":"19","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":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
-5110116099004204948
|
5522932483214936164
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
67
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), ['nx', 'ex' => $retryAfter]);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $this->executeRequest(fn () => $batchConfig['api']->read($batchReadRequest));
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (RateLimitException $e) {
throw $e;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
20525
|
NULL
|
NULL
|
NULL
|
|
20529
|
890
|
6
|
2026-05-11T15:47:23.183431+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514443183_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
67
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), ['nx', 'ex' => $retryAfter]);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $this->executeRequest(fn () => $batchConfig['api']->read($batchReadRequest));
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (RateLimitException $e) {
throw $e;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}...
|
[{"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-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"ClientTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'ClientTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'ClientTest'","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":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"67","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), ['nx', 'ex' => $retryAfter]);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n\n $response = $this->executeRequest(fn () => $batchConfig['api']->read($batchReadRequest));\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (RateLimitException $e) {\n throw $e;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), ['nx', 'ex' => $retryAfter]);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n\n $response = $this->executeRequest(fn () => $batchConfig['api']->read($batchReadRequest));\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (RateLimitException $e) {\n throw $e;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"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":"19","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":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8297783771627196609
|
5522932483223324772
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
67
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), ['nx', 'ex' => $retryAfter]);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $this->executeRequest(fn () => $batchConfig['api']->read($batchReadRequest));
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (RateLimitException $e) {
throw $e;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
20531
|
890
|
7
|
2026-05-11T15:47:53.621536+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514473621_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
67
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), ['nx', 'ex' => $retryAfter]);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $this->executeRequest(fn () => $batchConfig['api']->read($batchReadRequest));
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (RateLimitException $e) {
throw $e;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
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-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"ClientTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'ClientTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'ClientTest'","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":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"67","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), ['nx', 'ex' => $retryAfter]);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n\n $response = $this->executeRequest(fn () => $batchConfig['api']->read($batchReadRequest));\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (RateLimitException $e) {\n throw $e;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), ['nx', 'ex' => $retryAfter]);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n\n $response = $this->executeRequest(fn () => $batchConfig['api']->read($batchReadRequest));\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (RateLimitException $e) {\n throw $e;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"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":"19","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":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
-5110116099004204948
|
5522932483214936164
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
67
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), ['nx', 'ex' => $retryAfter]);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $this->executeRequest(fn () => $batchConfig['api']->read($batchReadRequest));
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (RateLimitException $e) {
throw $e;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
20529
|
NULL
|
NULL
|
NULL
|
|
20533
|
890
|
8
|
2026-05-11T15:48:24.041026+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514504041_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
67
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), ['nx', 'ex' => $retryAfter]);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $this->executeRequest(fn () => $batchConfig['api']->read($batchReadRequest));
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (RateLimitException $e) {
throw $e;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
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-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"ClientTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'ClientTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'ClientTest'","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":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"67","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), ['nx', 'ex' => $retryAfter]);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n\n $response = $this->executeRequest(fn () => $batchConfig['api']->read($batchReadRequest));\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (RateLimitException $e) {\n throw $e;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Illuminate\\Support\\Facades\\Redis;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Execute a HubSpot API call with rate limit handling.\n *\n * On a 429, stores the absolute expiry timestamp with SET NX (first writer wins).\n * This means all subsequent jobs that also receive 429 in the same burst do not\n * reset the TTL — the window is anchored to the first 429, not the last.\n * Readers compute the remaining wait from the stored timestamp, so jobs that check\n * the cache near expiry are not delayed longer than necessary.\n *\n * @template T\n *\n * @param callable(): T $apiCall The API call to execute\n *\n * @throws RateLimitException When rate limit is hit or cached rate limit is active\n *\n * @return T The result of the API call\n */\n private function executeRequest(callable $apiCall)\n {\n $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedExpiresAt = Redis::get($cacheKey);\n if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {\n $remaining = max(1, (int) $cachedExpiresAt - time());\n\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n $remaining,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n // NX: only the first job to receive a 429 in this burst sets the key.\n // Subsequent 429s in the same burst leave the TTL untouched so the\n // window is not reset by every concurrent job hitting the limit.\n Redis::set($cacheKey, (string) (time() + $retryAfter), ['nx', 'ex' => $retryAfter]);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $message = strtolower($e->getMessage());\n\n if (str_contains($message, 'daily')) {\n return 600;\n }\n if (str_contains($message, 'ten secondly')) {\n return 10;\n }\n if (str_contains($message, 'secondly')) {\n return 1;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or known message, using default', [\n 'exception_class' => get_class($e),\n 'message' => $message,\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n *\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n *\n * @return array The search response with 'results', 'total', 'paging' keys\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n });\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n\n $response = $this->executeRequest(fn () => $batchConfig['api']->read($batchReadRequest));\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (RateLimitException $e) {\n throw $e;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"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":"19","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":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
-5110116099004204948
|
5522932483214936164
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
ClientTest
Run 'ClientTest'
Debug 'ClientTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
67
3
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Illuminate\Support\Facades\Redis;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
*
* @param callable(): T $apiCall The API call to execute
*
* @throws RateLimitException When rate limit is hit or cached rate limit is active
*
* @return T The result of the API call
*/
private function executeRequest(callable $apiCall)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedExpiresAt = Redis::get($cacheKey);
if (is_string($cachedExpiresAt) && is_numeric($cachedExpiresAt)) {
$remaining = max(1, (int) $cachedExpiresAt - time());
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
$remaining,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
// NX: only the first job to receive a 429 in this burst sets the key.
// Subsequent 429s in the same burst leave the TTL untouched so the
// window is not reset by every concurrent job hitting the limit.
Redis::set($cacheKey, (string) (time() + $retryAfter), ['nx', 'ex' => $retryAfter]);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:config:%d', $this->config->getId());
}
private function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$message = strtolower($e->getMessage());
if (str_contains($message, 'daily')) {
return 600;
}
if (str_contains($message, 'ten secondly')) {
return 10;
}
if (str_contains($message, 'secondly')) {
return 1;
}
$this->log->warning('[Hubspot] No retry-after header or known message, using default', [
'exception_class' => get_class($e),
'message' => $message,
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
*
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*
* @return array The search response with 'results', 'total', 'paging' keys
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $this->executeRequest(fn () => $batchConfig['api']->read($batchReadRequest));
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (RateLimitException $e) {
throw $e;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
20535
|
890
|
9
|
2026-05-11T15:48:53.721817+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778514533721_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelpDOCKERAPP (-zsh)-zsh+ +- ₴81DEV (docker)₴2APP (-zsh)ScrmService->syncOpportunity('374720564');ScrmService-›matchByName('Robot');-zsh> 0 hhl*5screenpipe"100% <78• Mon 11 May 18:48:53T81O ₴6-zsh*7 |+end diffAPPFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1Learn moreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfixdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diffPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.3.30Running analysis on 7 cores with 10 files per process.Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!Loadedconfig default from".php-cs-fixer.dist.php".5666/5666 [100%Fixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image » docker debug docker_1amp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ I...
|
NULL
|
2428348653593949752
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelpDOCKERAPP (-zsh)-zsh+ +- ₴81DEV (docker)₴2APP (-zsh)ScrmService->syncOpportunity('374720564');ScrmService-›matchByName('Robot');-zsh> 0 hhl*5screenpipe"100% <78• Mon 11 May 18:48:53T81O ₴6-zsh*7 |+end diffAPPFixed 4 of 5666 files in 146.870 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1Learn moreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ csfixdocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diffPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.3.30Running analysis on 7 cores with 10 files per process.Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!Loadedconfig default from".php-cs-fixer.dist.php".5666/5666 [100%Fixed 0 of 5666 files in 66.457 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image » docker debug docker_1amp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ I...
|
20533
|
NULL
|
NULL
|
NULL
|